@parallel-cli/parallel 0.4.3 → 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/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, providerNeedsApiKey, providerReady, 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';
@@ -18,7 +18,7 @@ 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
20
  // Version from package.json. Hardcoded — rootDir: "src" prevents importing ../../package.json.
21
- const VERSION = '0.4.3';
21
+ const VERSION = '0.4.4';
22
22
  function usableProvider(config) {
23
23
  const p = getProvider(config);
24
24
  return p && providerReady(p) && (p.defaultModel || p.models[0]) ? p : undefined;
@@ -60,7 +60,7 @@ export function App({ config, initialFolder }) {
60
60
  const [modelCustom, setModelCustom] = useState(false);
61
61
  const ctlRef = useRef(directFolder ? new Controller(config, directFolder) : null);
62
62
  // ---------- main state ----------
63
- const [, setTick] = useState(0);
63
+ const [tick, setTick] = useState(0);
64
64
  const [view, setView] = useState('agents');
65
65
  // Focus mode (/focus <agent>): plain input is routed to that agent.
66
66
  const [focus, setFocus] = useState(null);
@@ -107,6 +107,12 @@ export function App({ config, initialFolder }) {
107
107
  clearInterval(timer);
108
108
  };
109
109
  }, [ctl]);
110
+ useEffect(() => {
111
+ if (!ctl || !focus)
112
+ return;
113
+ if (!ctl.board.getAgentByName(focus))
114
+ setFocus(null);
115
+ }, [ctl, focus, tick]);
110
116
  // Session server: lets `parallel attach <agent>` open per-agent terminals.
111
117
  useEffect(() => {
112
118
  if (!ctl)
@@ -228,7 +234,10 @@ export function App({ config, initialFolder }) {
228
234
  setProviderStep({ id: 'pick' });
229
235
  }
230
236
  else if (providerStep.id === 'endpoint') {
231
- setProviderStep({ id: 'presetModel', provider: providerStep.provider });
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 });
232
241
  }
233
242
  else if (providerStep.id === 'editEndpoint') {
234
243
  setProviderStep({ id: 'endpoint', provider: providerStep.provider });
@@ -391,27 +400,16 @@ export function App({ config, initialFolder }) {
391
400
  ...ls.slice(-5),
392
401
  { text: t('wiz.provider.ollama.checking', { url: preset.baseUrl }), level: 'info' },
393
402
  ]);
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
- }
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
+ ]);
413
411
  }
414
- catch {
412
+ else {
415
413
  setSystemLines((ls) => [
416
414
  ...ls.slice(-5),
417
415
  { text: t('wiz.provider.ollama.notFound', { url: preset.baseUrl }), level: 'warn' },
@@ -452,16 +450,35 @@ export function App({ config, initialFolder }) {
452
450
  if (providerNeedsApiKey(providerStep.provider))
453
451
  return setProviderStep({ id: 'key', provider: providerStep.provider });
454
452
  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({
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({
465
482
  ...providerStep.provider,
466
483
  apiKey: key.trim(),
467
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: [
@@ -548,8 +565,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
548
565
  : [];
549
566
  const [hubScroll, setHubScroll] = useState(0);
550
567
  const [hubFollowTail, setHubFollowTail] = useState(true);
551
- const hubRows = Math.max(6, bodyHeight - 2);
552
- 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)));
553
570
  const clampedHub = Math.min(hubScroll, maxHubScroll);
554
571
  const logSeq = ctl.board.logs.length > 0 ? ctl.board.logs[ctl.board.logs.length - 1].seq ?? ctl.board.logs.length : 0;
555
572
  useEffect(() => {
@@ -560,7 +577,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
560
577
  if (hubFollowTail)
561
578
  setHubScroll(0);
562
579
  }, [logSeq, agents.length, hubFollowTail]);
563
- // Scroll helpers (also used by mouse wheel handler below).
580
+ // Scroll helpers.
564
581
  const scrollFocusUp = () => {
565
582
  setFocusFollowTail(false);
566
583
  setScroll((s) => Math.min(s + 1, maxScroll));
@@ -585,25 +602,23 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
585
602
  return next;
586
603
  });
587
604
  };
588
- // Keyboard: Esc / PgUp-PgDn / Up-Down arrows.
589
- // When CommandInput is NOT focused, Up/Down scroll the hub or focused agent;
590
- // 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.
591
606
  useInput((_input, key) => {
592
- if (key.escape)
607
+ if (key.escape && !settingsOpen)
593
608
  onEscape();
594
609
  if (focused) {
595
- if (key.pageUp || key.upArrow)
610
+ if (key.pageUp || (!inputActive && key.upArrow))
596
611
  scrollFocusUp();
597
- if (key.pageDown || key.downArrow)
612
+ if (key.pageDown || (!inputActive && key.downArrow))
598
613
  scrollFocusDown();
599
614
  }
600
615
  else if (view === 'agents') {
601
- if (key.pageUp || key.upArrow)
616
+ if (key.pageUp || (!inputActive && key.upArrow))
602
617
  scrollHubUp();
603
- if (key.pageDown || key.downArrow)
618
+ if (key.pageDown || (!inputActive && key.downArrow))
604
619
  scrollHubDown();
605
620
  }
606
- }, { isActive: !inputActive });
621
+ }, { isActive: !approval && !question });
607
622
  const idleCount = agents.filter((a) => a.state === 'idle').length;
608
623
  const workingCount = agents.filter((a) => ['working', 'thinking', 'listening'].includes(a.state)).length;
609
624
  const doneCount = agents.filter((a) => a.state === 'done').length;
@@ -627,7 +642,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
627
642
  specialists: 'specialists',
628
643
  };
629
644
  const viewLabel = VIEW_LABEL[view] ?? 'control room';
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
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
631
646
  ? systemLines
632
647
  .filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
633
648
  .slice(-2)
@@ -658,27 +673,33 @@ function groupAgents(agents) {
658
673
  function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
659
674
  const groups = groupAgents([...agents].sort((a, b) => STATE_META[a.state].rank - STATE_META[b.state].rank || a.startedAt - b.startedAt));
660
675
  let skipped = scroll;
661
- let rendered = 0;
676
+ let renderedAgents = 0;
677
+ let renderedLines = 0;
662
678
  const rows = [];
679
+ let full = false;
663
680
  for (const group of groups) {
664
- const groupRows = [];
665
681
  for (const agent of group.agents) {
666
682
  if (skipped > 0) {
667
683
  skipped--;
668
684
  continue;
669
685
  }
670
- if (rendered >= visibleRows)
671
- continue;
672
- rendered++;
673
- groupRows.push(_jsx(AgentRow, { agent: agent, logs: ctl.board.logsFor(agent.id, 8), cols: cols }, agent.id));
674
- }
675
- if (groupRows.length === 0)
676
- continue;
677
- if (rows.length > 0) {
678
- rows.push(_jsx(Text, { color: CHROME.separator, children: '─'.repeat(cols - 2) }, `sep-${group.title}`));
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));
679
699
  }
680
- rows.push(_jsx(Box, { flexDirection: "column", children: groupRows }, group.title));
700
+ if (full)
701
+ break;
681
702
  }
682
- const below = Math.max(0, agents.length - scroll - rendered);
703
+ const below = Math.max(0, agents.length - scroll - renderedAgents);
683
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] }));
684
705
  }
@@ -3,9 +3,9 @@ import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { createSkillTemplate, createSpecialistTemplate } from '../skills.js';
5
5
  import { priceFor } from '../pricing.js';
6
- import { SelectList } from './Wizard.js';
6
+ import { SelectList as BaseSelectList } from './Wizard.js';
7
7
  import { LANGS, getLang, setLang, t } from '../i18n.js';
8
- import { providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
8
+ import { detectProviderModels, isLocalProvider, isPlaceholderModel, providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
9
9
  function masked(key) {
10
10
  if (!key)
11
11
  return '—';
@@ -39,6 +39,16 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
39
39
  const saved = () => setFlash(t('set.saved'));
40
40
  const cfg = ctl.config;
41
41
  const listHeight = height ? Math.max(3, height - 5) : undefined;
42
+ const goBack = () => {
43
+ if (step.id === 'root')
44
+ return onClose();
45
+ setStep(returnStep ?? { id: 'root' });
46
+ setReturnStep(null);
47
+ };
48
+ const SelectList = (props) => {
49
+ const { onBack, ...rest } = props;
50
+ return _jsx(BaseSelectList, { ...rest, onBack: onBack ?? goBack });
51
+ };
42
52
  // ---- root menu items ----
43
53
  const rootItems = scope === 'global'
44
54
  ? [
@@ -103,6 +113,10 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
103
113
  };
104
114
  // ---- shared helpers ----
105
115
  const pickModel = (provider, model) => {
116
+ if (isPlaceholderModel(model)) {
117
+ setFlash(t('set.modelPlaceholder'));
118
+ return;
119
+ }
106
120
  if (step.id === 'model' && step.setup) {
107
121
  provider.defaultModel = model;
108
122
  if (!provider.models.includes(model))
@@ -124,23 +138,25 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
124
138
  setStep(returnStep ?? { id: 'root' });
125
139
  setReturnStep(null);
126
140
  };
127
- const finishProviderSetup = (provider) => {
128
- ctl.saveProvider(provider);
129
- if (scope === 'global') {
130
- ctl.setDefaultProvider(provider.name);
131
- saved();
141
+ const finishProviderSetup = (provider, persist = scope === 'global') => {
142
+ if (persist) {
143
+ ctl.saveProvider(provider);
144
+ if (scope === 'global') {
145
+ ctl.setDefaultProvider(provider.name);
146
+ saved();
147
+ }
148
+ else {
149
+ ctl.setSessionModel(`${provider.name}:${provider.defaultModel || provider.models[0] || ''}`);
150
+ setFlash(t('set.saved'));
151
+ }
132
152
  }
133
153
  else {
134
- ctl.setSessionModel(`${provider.name}:${provider.defaultModel || provider.models[0] || ''}`);
154
+ ctl.setSessionProviderConfig(provider);
155
+ setFlash(t('set.sessionProviderReady', { name: provider.name }));
135
156
  }
136
157
  setStep(returnStep ?? { id: 'providers', scope });
137
158
  setReturnStep(null);
138
159
  };
139
- const finishNewProvider = (name, url, model, key) => {
140
- finishProviderSetup({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
141
- setStep(returnStep ?? { id: 'root' });
142
- setReturnStep(null);
143
- };
144
160
  // ---- navigate into a sub-step, remembering where to return ----
145
161
  const goSub = (next) => {
146
162
  setReturnStep(step);
@@ -194,6 +210,8 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
194
210
  if (step.setup) {
195
211
  if (providerNeedsApiKey(step.provider))
196
212
  return setStep({ id: 'key', provider: step.provider, setup: true });
213
+ if (scope === 'session')
214
+ return setStep({ id: 'setupScope', provider: step.provider });
197
215
  finishProviderSetup(step.provider);
198
216
  return;
199
217
  }
@@ -201,6 +219,14 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
201
219
  saved();
202
220
  setStep(returnStep ?? { id: 'providerDetail', provider: step.provider, scope });
203
221
  setReturnStep(null);
222
+ } })] })), step.id === 'setupScope' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.setupScope.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
223
+ { label: t('set.setupScope.session'), value: 'session' },
224
+ { label: t('set.setupScope.global'), value: 'global' },
225
+ { label: t('set.back'), value: '__back__' },
226
+ ], height: listHeight, onSelect: (v) => {
227
+ if (v === '__back__')
228
+ return setStep({ id: 'endpoint', provider: step.provider, setup: true });
229
+ finishProviderSetup(step.provider, v === 'global');
204
230
  } })] })), step.id === 'editEndpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: step.provider.baseUrl, onInput: (url) => {
205
231
  const provider = { ...step.provider, baseUrl: url.trim() };
206
232
  setStep({ id: 'endpoint', provider, setup: step.setup });
@@ -268,15 +294,40 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
268
294
  setReturnStep(null);
269
295
  } })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
270
296
  const provider = { ...step.provider, apiKey: k.trim() };
271
- if (step.setup)
297
+ if (step.setup) {
298
+ if (scope === 'session') {
299
+ setStep({ id: 'setupScope', provider });
300
+ return;
301
+ }
272
302
  finishProviderSetup(provider);
303
+ return;
304
+ }
273
305
  else {
274
306
  ctl.saveProvider(provider);
275
307
  saved();
276
308
  }
277
309
  setStep(returnStep ?? { id: 'root' });
278
310
  setReturnStep(null);
279
- } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onInput: (name) => setStep({ id: 'newUrl', name }) })] })), step.id === 'newUrl' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onInput: (url) => setStep({ id: 'newModel', name: step.name, url }) })] })), step.id === 'newModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.model.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => setStep({ id: 'newKey', name: step.name, url: step.url, model }) })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishNewProvider(step.name, step.url, step.model, key.trim()) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
311
+ } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onInput: (name) => setStep({ id: 'newUrl', name }) })] })), step.id === 'newUrl' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onInput: (url) => setStep({ id: 'newModel', name: step.name, url }) })] })), step.id === 'newModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.model.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => {
312
+ const trimmed = model.trim();
313
+ if (isPlaceholderModel(trimmed)) {
314
+ setFlash(t('set.modelPlaceholder'));
315
+ return;
316
+ }
317
+ const local = isLocalProvider({ baseUrl: step.url });
318
+ setStep({
319
+ id: 'endpoint',
320
+ setup: true,
321
+ provider: {
322
+ name: step.name,
323
+ baseUrl: step.url,
324
+ apiKey: '',
325
+ models: [trimmed],
326
+ defaultModel: trimmed,
327
+ requiresApiKey: !local,
328
+ },
329
+ });
330
+ } })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishProviderSetup({ name: step.name, baseUrl: step.url, apiKey: key.trim(), models: [step.model], defaultModel: step.model }) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
280
331
  try {
281
332
  const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
282
333
  setFlash(t('m.skillCreated', { file }));
@@ -355,9 +406,26 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
355
406
  const preset = PROVIDER_PRESETS.find((p) => p.name === presetName);
356
407
  if (!preset)
357
408
  return;
358
- if (presetName.toLowerCase() === 'ollama') {
359
- setReturnStep({ id: 'providers', scope: step.scope });
360
- return setStep({ id: 'model', provider: { ...preset, apiKey: 'ollama-local' }, setup: true });
409
+ if (preset.category === 'local') {
410
+ setFlash(t('wiz.provider.ollama.checking', { url: preset.baseUrl }));
411
+ void (async () => {
412
+ const detected = await detectProviderModels(preset);
413
+ setFlash(detected
414
+ ? t('wiz.provider.ollama.found', { n: detected.models.length })
415
+ : t('wiz.provider.ollama.notFound', { url: preset.baseUrl }));
416
+ setReturnStep({ id: 'providers', scope: step.scope });
417
+ setStep({
418
+ id: 'model',
419
+ provider: {
420
+ ...preset,
421
+ apiKey: 'local',
422
+ models: detected?.models ?? [...preset.models],
423
+ defaultModel: detected?.defaultModel ?? preset.defaultModel,
424
+ },
425
+ setup: true,
426
+ });
427
+ })();
428
+ return;
361
429
  }
362
430
  // Preset setup: choose model, review endpoint, then ask for API key if needed.
363
431
  setReturnStep({ id: 'providers', scope: step.scope });
package/dist/ui/views.js CHANGED
@@ -52,20 +52,27 @@ function useVisibleRows(overhead, min = 6) {
52
52
  const { stdout } = useStdout();
53
53
  return Math.max(min, (stdout?.rows ?? 30) - overhead);
54
54
  }
55
- export function BoardView({ board }) {
55
+ export function BoardView({ board, bodyHeight }) {
56
56
  const agents = [...board.agents.values()];
57
- const activities = [...board.fileActivity.values()].sort((a, b) => b.ts - a.ts).slice(0, 12);
58
- return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('board.title') }), _jsx(Text, { bold: true, children: t('board.agents') }), agents.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.none')] })) : (agents.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name }), _jsxs(Text, { color: STATE_LABEL[a.state].color, children: [' ', STATE_LABEL[a.state].icon, " ", stateLabel(a.state)] }), _jsxs(Text, { color: "gray", children: [" ", truncate(a.currentAction || a.task, 110)] })] }, a.id)))), _jsx(Text, { bold: true, children: t('board.activity') }), activities.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.noActivity')] })) : (activities.map((act) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', "\u270F ", act.path, " ", _jsxs(Text, { color: "gray", children: ["\u2014 ", act.agentName, " (", act.op, ", ", Math.round((Date.now() - act.ts) / 1000), "s)"] })] }, act.path)))), _jsx(Text, { bold: true, children: t('board.notes') }), board.notes.slice(-8).map((n) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "magenta", children: [n.from, " \u2192 ", n.to] }), _jsxs(Text, { children: [": ", truncate(n.content, 140)] })] }, n.id)))] }));
57
+ const fallbackVisible = useVisibleRows(12);
58
+ const visibleAgents = bodyHeight ? Math.max(1, Math.floor((bodyHeight - 7) / 3)) : fallbackVisible;
59
+ const { slice: agentSlice, above, below } = useScrollWindow(agents, visibleAgents, 'top');
60
+ const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents - 5) / 2)) : 8;
61
+ const activities = [...board.fileActivity.values()].sort((a, b) => b.ts - a.ts).slice(0, sideRows);
62
+ const notes = board.notes.slice(-sideRows);
63
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('board.title') }), _jsx(Text, { bold: true, children: t('board.agents') }), agents.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.none')] })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), agentSlice.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name }), _jsxs(Text, { color: STATE_LABEL[a.state].color, children: [' ', STATE_LABEL[a.state].icon, " ", stateLabel(a.state)] }), _jsxs(Text, { color: "gray", children: [" ", truncate(a.currentAction || a.task, 110)] })] }, a.id))), _jsx(Below, { n: below })] })), _jsx(Text, { bold: true, children: t('board.activity') }), activities.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.noActivity')] })) : (activities.map((act) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', "\u270F ", act.path, " ", _jsxs(Text, { color: "gray", children: ["\u2014 ", act.agentName, " (", act.op, ", ", Math.round((Date.now() - act.ts) / 1000), "s)"] })] }, act.path)))), _jsx(Text, { bold: true, children: t('board.notes') }), notes.map((n) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "magenta", children: [n.from, " \u2192 ", n.to] }), _jsxs(Text, { children: [": ", truncate(n.content, 140)] })] }, n.id)))] }));
59
64
  }
60
- export function NotesView({ board }) {
61
- const visible = useVisibleRows(7);
65
+ export function NotesView({ board, bodyHeight }) {
66
+ const fallbackVisible = useVisibleRows(7);
67
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 4) : fallbackVisible;
62
68
  const { slice, above, below } = useScrollWindow(board.notes, visible, 'bottom');
63
69
  return (_jsxs(Box, { borderStyle: "round", borderColor: "magenta", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: t('notes.title') }), board.notes.length === 0 ? (_jsx(Text, { color: "gray", children: t('notes.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((n) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: "gray", children: [new Date(n.ts).toLocaleTimeString(), " "] }), _jsx(Text, { color: "magenta", bold: true, children: n.from }), _jsxs(Text, { color: "gray", children: [" \u2192 ", n.to, ": "] }), _jsx(Text, { children: truncate(n.content, 200) })] }, n.id))), _jsx(Below, { n: below })] }))] }));
64
70
  }
65
- export function DiffView({ board }) {
71
+ export function DiffView({ board, bodyHeight }) {
66
72
  // Each change renders up to ~33 rows (header + 30 patch lines + spacing):
67
73
  // window over WHOLE history, newest first, PgUp to walk back in time.
68
- const rows = useVisibleRows(8, 18);
74
+ const fallbackRows = useVisibleRows(8, 18);
75
+ const rows = bodyHeight ? Math.max(8, bodyHeight - 4) : fallbackRows;
69
76
  const perChange = Math.max(1, Math.floor(rows / 34));
70
77
  const { slice: changes, above, below } = useScrollWindow(board.changes, perChange, 'bottom');
71
78
  return (_jsxs(Box, { borderStyle: "round", borderColor: "green", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: t('diff.title', { total: board.changes.length }) }), board.changes.length === 0 ? (_jsx(Text, { color: "gray", children: t('diff.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), changes.map((c) => {
@@ -75,24 +82,40 @@ export function DiffView({ board }) {
75
82
  }), _jsx(Below, { n: below })] }))] }));
76
83
  }
77
84
  /** Financial view: live cost / steps / tokens per agent + session total. */
78
- export function CostView({ board }) {
85
+ export function CostView({ board, bodyHeight }) {
79
86
  const agents = [...board.agents.values()];
87
+ const fallbackVisible = useVisibleRows(8);
88
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 7) : fallbackVisible;
89
+ const { slice, above, below } = useScrollWindow(agents, visible, 'top');
80
90
  const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
81
91
  const unknown = agents.some((a) => a.cost === null);
82
- return (_jsxs(Box, { borderStyle: "round", borderColor: "greenBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "greenBright", children: t('cost.title') }), agents.length === 0 ? (_jsx(Text, { color: "gray", children: t('cost.empty') })) : (_jsxs(_Fragment, { children: [agents.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name.padEnd(12) }), _jsxs(Text, { color: "gray", children: [a.model.padEnd(24).slice(0, 24), " "] }), _jsxs(Text, { children: [String(a.steps).padStart(3), " steps "] }), _jsxs(Text, { color: "cyan", children: [String(Math.round(a.tokensIn / 1000)).padStart(5), "k in ", String(Math.round(a.tokensOut / 1000)).padStart(4), "k out", ' '] }), _jsx(Text, { color: "greenBright", bold: true, children: a.cost === null ? ' $—' : fmtCost(a.cost).padStart(8) }), a.cost === null ? _jsxs(Text, { color: "gray", children: [" ", t('cost.unknown')] }) : null] }, a.id))), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [' ', t('cost.total'), " ", _jsx(Text, { color: "greenBright", children: fmtCost(total) }), unknown ? _jsxs(Text, { color: "gray", children: [" ", t('cost.partial')] }) : null] })] })), _jsx(Text, { color: "gray", children: t('cost.hint') })] }));
92
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "greenBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "greenBright", children: t('cost.title') }), agents.length === 0 ? (_jsx(Text, { color: "gray", children: t('cost.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name.padEnd(12) }), _jsxs(Text, { color: "gray", children: [a.model.padEnd(24).slice(0, 24), " "] }), _jsxs(Text, { children: [String(a.steps).padStart(3), " steps "] }), _jsxs(Text, { color: "cyan", children: [String(Math.round(a.tokensIn / 1000)).padStart(5), "k in ", String(Math.round(a.tokensOut / 1000)).padStart(4), "k out", ' '] }), _jsx(Text, { color: "greenBright", bold: true, children: a.cost === null ? ' $—' : fmtCost(a.cost).padStart(8) }), a.cost === null ? _jsxs(Text, { color: "gray", children: [" ", t('cost.unknown')] }) : null] }, a.id))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [' ', t('cost.total'), " ", _jsx(Text, { color: "greenBright", children: fmtCost(total) }), unknown ? _jsxs(Text, { color: "gray", children: [" ", t('cost.partial')] }) : null] })] })), _jsx(Text, { color: "gray", children: t('cost.hint') })] }));
83
93
  }
84
94
  /** Skills catalog: user-authored markdown instructions agents can load. */
85
- export function SkillsView({ skills }) {
86
- return (_jsxs(Box, { borderStyle: "round", borderColor: "blueBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "blueBright", children: t('skills.title') }), skills.length === 0 ? (_jsx(Text, { color: "gray", children: t('skills.empty') })) : (skills.map((s) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "blueBright", bold: true, children: ["#", s.name.padEnd(16)] }), _jsxs(Text, { color: s.scope === 'global' ? 'yellow' : 'green', children: ["[", s.scope, "] "] }), _jsx(Text, { color: "gray", children: truncate(s.description || s.file, 100) })] }, s.file)))), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('skills.hint1') }), _jsx(Text, { color: "gray", children: t('skills.hint2') })] }));
95
+ export function SkillsView({ skills, bodyHeight }) {
96
+ const fallbackVisible = useVisibleRows(8);
97
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
98
+ const { slice, above, below } = useScrollWindow(skills, visible, 'top');
99
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "blueBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "blueBright", children: t('skills.title') }), skills.length === 0 ? (_jsx(Text, { color: "gray", children: t('skills.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((s) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "blueBright", bold: true, children: ["#", s.name.padEnd(16)] }), _jsxs(Text, { color: s.scope === 'global' ? 'yellow' : 'green', children: ["[", s.scope, "] "] }), _jsx(Text, { color: "gray", children: truncate(s.description || s.file, 100) })] }, s.file))), _jsx(Below, { n: below })] })), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('skills.hint1') }), _jsx(Text, { color: "gray", children: t('skills.hint2') })] }));
87
100
  }
88
101
  /** Specialists catalog: personas (role + optional pinned model). */
89
- export function SpecialistsView({ specialists }) {
90
- return (_jsxs(Box, { borderStyle: "round", borderColor: "magentaBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magentaBright", children: t('spec.title') }), specialists.length === 0 ? (_jsx(Text, { color: "gray", children: t('spec.empty') })) : (specialists.map((s) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "magentaBright", bold: true, children: ["\uD83C\uDF93", s.name.padEnd(16)] }), _jsxs(Text, { color: s.scope === 'global' ? 'yellow' : 'green', children: ["[", s.scope, "] "] }), s.model ? _jsxs(Text, { color: "cyan", children: [s.model, " "] }) : null, _jsx(Text, { color: "gray", children: truncate(s.description || s.file, 90) })] }, s.file)))), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('spec.hint1') }), _jsx(Text, { color: "gray", children: t('spec.hint2') })] }));
102
+ export function SpecialistsView({ specialists, bodyHeight }) {
103
+ const fallbackVisible = useVisibleRows(8);
104
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
105
+ const { slice, above, below } = useScrollWindow(specialists, visible, 'top');
106
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "magentaBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magentaBright", children: t('spec.title') }), specialists.length === 0 ? (_jsx(Text, { color: "gray", children: t('spec.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((s) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "magentaBright", bold: true, children: ["\uD83C\uDF93", s.name.padEnd(16)] }), _jsxs(Text, { color: s.scope === 'global' ? 'yellow' : 'green', children: ["[", s.scope, "] "] }), s.model ? _jsxs(Text, { color: "cyan", children: [s.model, " "] }) : null, _jsx(Text, { color: "gray", children: truncate(s.description || s.file, 90) })] }, s.file))), _jsx(Below, { n: below })] })), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('spec.hint1') }), _jsx(Text, { color: "gray", children: t('spec.hint2') })] }));
91
107
  }
92
108
  /** Saved sessions: inspect available restore points; restore via /session. */
93
- export function SessionsView({ projectRoot }) {
109
+ export function SessionsView({ projectRoot, bodyHeight }) {
94
110
  const sessions = Controller.listSessions(projectRoot);
95
- return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('sessions.title') }), sessions.length === 0 ? (_jsx(Text, { color: "gray", children: t('sessions.empty') })) : (sessions.map((s, i) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "yellow", bold: true, children: [String(i + 1).padStart(2), "."] }), ' ', _jsx(Text, { children: t('sessions.item', { date: new Date(s.data.savedAt).toLocaleString(), agents: s.data.agents.length }) }), _jsxs(Text, { color: "gray", children: [" ", s.data.agents.map((a) => a.name).join(', ').slice(0, 80)] })] }, s.file)))), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('sessions.hint') })] }));
111
+ const fallbackVisible = useVisibleRows(7);
112
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 5) : fallbackVisible;
113
+ const { slice, above, below } = useScrollWindow(sessions, visible, 'top');
114
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('sessions.title') }), sessions.length === 0 ? (_jsx(Text, { color: "gray", children: t('sessions.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((s, i) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "yellow", bold: true, children: [String(sessions.indexOf(s) + 1).padStart(2), "."] }), ' ', _jsx(Text, { children: t('sessions.item', {
115
+ name: s.data.name ? `${s.data.name} · ` : '',
116
+ date: new Date(s.data.savedAt).toLocaleString(),
117
+ agents: s.data.agents.length,
118
+ }) }), _jsxs(Text, { color: "gray", children: [" ", s.data.agents.map((a) => a.name).join(', ').slice(0, 80)] })] }, s.file))), _jsx(Below, { n: below })] })), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('sessions.hint') })] }));
96
119
  }
97
120
  export function HelpView({ bodyHeight }) {
98
121
  // Fixed intro/highlight/footer rows consume about 12 lines inside the already-sized body.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallel-cli/parallel",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Real-time multi-agent coding CLI with shared context, adaptive co-editing, dedicated agent terminals, and headless CI runs.",
5
5
  "keywords": [
6
6
  "cli",