@parallel-cli/parallel 0.4.3 → 0.4.5

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.
@@ -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 });
@@ -26,20 +26,22 @@ function itemColor(item) {
26
26
  return UI.text;
27
27
  return UI.text;
28
28
  }
29
- function OutputLines({ item }) {
29
+ function OutputLines({ item, cols }) {
30
30
  if (!item.output || item.output.length === 0)
31
31
  return null;
32
- return (_jsxs(Box, { flexDirection: "column", children: [item.output.map((line, i) => (_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.muted, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: i === 0 ? '└ ' : ' ' }), truncate(line, 180)] }, `${item.seq ?? 0}-out-${i}`))), item.hiddenLines && item.hiddenLines > 0 ? (_jsxs(Text, { color: UI.muted, children: [' ', t('timeline.hiddenLines', { count: item.hiddenLines })] })) : null] }));
32
+ const max = Math.max(40, cols - 8);
33
+ return (_jsxs(Box, { flexDirection: "column", children: [item.output.map((line, i) => (_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.muted, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: i === 0 ? '└ ' : ' ' }), truncate(line, max)] }, `${item.seq ?? 0}-out-${i}`))), item.hiddenLines && item.hiddenLines > 0 ? (_jsxs(Text, { color: UI.muted, children: [' ', t('timeline.hiddenLines', { count: item.hiddenLines })] })) : null] }));
33
34
  }
34
- function TimelineRow({ item }) {
35
+ function TimelineRow({ item, cols }) {
36
+ const max = Math.max(40, cols - 8);
35
37
  if (item.kind === 'section') {
36
- return _jsx(Text, { color: UI.muted, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" });
38
+ return _jsx(Text, { color: UI.muted, children: '─'.repeat(Math.min(Math.max(20, cols - 4), 80)) });
37
39
  }
38
40
  if (item.kind === 'narration') {
39
41
  return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: UI.text, wrap: "wrap", children: item.detail }) }));
40
42
  }
41
43
  if (item.kind === 'command') {
42
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.text, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [t('timeline.ran'), " "] }), _jsx(Text, { color: UI.accent, children: truncate(item.command ?? '', 160) })] }), _jsx(OutputLines, { item: item })] }));
44
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.text, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [t('timeline.ran'), " "] }), _jsx(Text, { color: UI.accent, children: truncate(item.command ?? '', max) })] }), _jsx(OutputLines, { item: item, cols: cols })] }));
43
45
  }
44
46
  if (item.kind === 'files') {
45
47
  const files = item.files ?? [];
@@ -48,13 +50,13 @@ function TimelineRow({ item }) {
48
50
  return (_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [fileLabel(item.label, files.length), " "] }), _jsxs(Text, { color: UI.muted, children: [shown, extra] })] }));
49
51
  }
50
52
  if (item.output) {
51
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), item.label] }), _jsx(OutputLines, { item: item })] }));
53
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), item.label] }), _jsx(OutputLines, { item: item, cols: cols })] }));
52
54
  }
53
- return (_jsxs(Text, { color: itemColor(item), italic: item.kind === 'thought', wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), truncate(item.detail ? `${item.label} ${item.detail}` : item.label, 180)] }));
55
+ return (_jsxs(Text, { color: itemColor(item), italic: item.kind === 'thought', wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), truncate(item.detail ? `${item.label} ${item.detail}` : item.label, max)] }));
54
56
  }
55
- export function Timeline({ logs, raw = false, emptyText }) {
57
+ export function Timeline({ logs, raw = false, emptyText, cols = 100 }) {
56
58
  const items = presentTimeline(logs, { raw, outputLines: raw ? 10 : 6 });
57
59
  if (items.length === 0)
58
60
  return _jsx(Text, { color: UI.muted, children: emptyText ?? t('timeline.empty') });
59
- return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => item.kind === 'section' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TimelineRow, { item: item }), _jsx(Text, { color: UI.muted, children: sectionLabel(item.category) })] }, `${item.seq ?? i}-section`)) : (_jsx(TimelineRow, { item: item }, `${item.seq ?? i}-${i}`))) }));
61
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => item.kind === 'section' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TimelineRow, { item: item, cols: cols }), _jsx(Text, { color: UI.muted, children: sectionLabel(item.category) })] }, `${item.seq ?? i}-section`)) : (_jsx(TimelineRow, { item: item, cols: cols }, `${item.seq ?? i}-${i}`))) }));
60
62
  }
package/dist/ui/views.js CHANGED
@@ -52,20 +52,28 @@ 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
+ const warnings = board.workMapWarnings.slice(-Math.max(2, Math.min(4, sideRows)));
64
+ 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, 80)] }), a.claims && a.claims.length > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 ", truncate(a.claims.join(', '), 45)] }) : null] }, a.id))), _jsx(Below, { n: below })] })), _jsx(Text, { bold: true, children: t('board.activity') }), warnings.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "yellowBright", children: t('board.workMap') }), warnings.map((w) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: w.level === 'conflict' ? 'redBright' : 'yellow', children: [w.level === 'conflict' ? '!' : '⚠', " "] }), _jsx(Text, { color: "yellow", children: w.title }), _jsxs(Text, { color: "gray", children: [" \u2014 ", truncate(w.detail, 120)] })] }, w.id)))] })) : null, 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
65
  }
60
- export function NotesView({ board }) {
61
- const visible = useVisibleRows(7);
66
+ export function NotesView({ board, bodyHeight }) {
67
+ const fallbackVisible = useVisibleRows(7);
68
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 4) : fallbackVisible;
62
69
  const { slice, above, below } = useScrollWindow(board.notes, visible, 'bottom');
63
70
  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
71
  }
65
- export function DiffView({ board }) {
72
+ export function DiffView({ board, bodyHeight }) {
66
73
  // Each change renders up to ~33 rows (header + 30 patch lines + spacing):
67
74
  // window over WHOLE history, newest first, PgUp to walk back in time.
68
- const rows = useVisibleRows(8, 18);
75
+ const fallbackRows = useVisibleRows(8, 18);
76
+ const rows = bodyHeight ? Math.max(8, bodyHeight - 4) : fallbackRows;
69
77
  const perChange = Math.max(1, Math.floor(rows / 34));
70
78
  const { slice: changes, above, below } = useScrollWindow(board.changes, perChange, 'bottom');
71
79
  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 +83,40 @@ export function DiffView({ board }) {
75
83
  }), _jsx(Below, { n: below })] }))] }));
76
84
  }
77
85
  /** Financial view: live cost / steps / tokens per agent + session total. */
78
- export function CostView({ board }) {
86
+ export function CostView({ board, bodyHeight }) {
79
87
  const agents = [...board.agents.values()];
88
+ const fallbackVisible = useVisibleRows(8);
89
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 7) : fallbackVisible;
90
+ const { slice, above, below } = useScrollWindow(agents, visible, 'top');
80
91
  const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
81
92
  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') })] }));
93
+ 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
94
  }
84
95
  /** 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') })] }));
96
+ export function SkillsView({ skills, bodyHeight }) {
97
+ const fallbackVisible = useVisibleRows(8);
98
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
99
+ const { slice, above, below } = useScrollWindow(skills, visible, 'top');
100
+ 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
101
  }
88
102
  /** 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') })] }));
103
+ export function SpecialistsView({ specialists, bodyHeight }) {
104
+ const fallbackVisible = useVisibleRows(8);
105
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
106
+ const { slice, above, below } = useScrollWindow(specialists, visible, 'top');
107
+ 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
108
  }
92
109
  /** Saved sessions: inspect available restore points; restore via /session. */
93
- export function SessionsView({ projectRoot }) {
110
+ export function SessionsView({ projectRoot, bodyHeight }) {
94
111
  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') })] }));
112
+ const fallbackVisible = useVisibleRows(7);
113
+ const visible = bodyHeight ? Math.max(3, bodyHeight - 5) : fallbackVisible;
114
+ const { slice, above, below } = useScrollWindow(sessions, visible, 'top');
115
+ 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', {
116
+ name: s.data.name ? `${s.data.name} · ` : '',
117
+ date: new Date(s.data.savedAt).toLocaleString(),
118
+ agents: s.data.agents.length,
119
+ }) }), _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
120
  }
97
121
  export function HelpView({ bodyHeight }) {
98
122
  // 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.5",
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",