@parallel-cli/parallel 0.4.4 → 0.4.6

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/Md.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { BRAND } from './tokens.js';
3
4
  /**
4
5
  * Mini markdown renderer for agent summaries — line-based, zero deps.
5
6
  * Supports: ## headers, - / * bullets, numbered lists, **bold**, `code`.
@@ -25,15 +26,15 @@ export function Md({ text, dim }) {
25
26
  return _jsx(Text, { children: " " }, i);
26
27
  const header = line.match(/^#{1,3}\s+(.*)$/);
27
28
  if (header) {
28
- return (_jsx(Text, { bold: true, color: "cyanBright", children: header[1] }, i));
29
+ return (_jsx(Text, { bold: true, color: BRAND.primary, children: header[1] }, i));
29
30
  }
30
31
  const bullet = line.match(/^(\s*)[-*]\s+(.*)$/);
31
32
  if (bullet) {
32
- return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [bullet[1], _jsx(Text, { color: "cyan", children: "\u2022 " }), inline(bullet[2], `b${i}`)] }, i));
33
+ return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [bullet[1], _jsx(Text, { color: BRAND.primary, children: "\u2022 " }), inline(bullet[2], `b${i}`)] }, i));
33
34
  }
34
35
  const numbered = line.match(/^(\s*)(\d+)[.)]\s+(.*)$/);
35
36
  if (numbered) {
36
- return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [numbered[1], _jsxs(Text, { color: "cyan", children: [numbered[2], ". "] }), inline(numbered[3], `n${i}`)] }, i));
37
+ return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [numbered[1], _jsxs(Text, { color: BRAND.primary, children: [numbered[2], ". "] }), inline(numbered[3], `n${i}`)] }, i));
37
38
  }
38
39
  return (_jsx(Text, { wrap: "wrap", dimColor: dim, children: inline(line, `l${i}`) }, i));
39
40
  }) }));
@@ -6,6 +6,7 @@ import { priceFor } from '../pricing.js';
6
6
  import { SelectList as BaseSelectList } from './Wizard.js';
7
7
  import { LANGS, getLang, setLang, t } from '../i18n.js';
8
8
  import { detectProviderModels, isLocalProvider, isPlaceholderModel, providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
9
+ import { BRAND } from './tokens.js';
9
10
  function masked(key) {
10
11
  if (!key)
11
12
  return '—';
@@ -163,7 +164,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
163
164
  setStep(next);
164
165
  };
165
166
  // ---- render ----
166
- return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: scope === 'global' ? t('set.title') : t('sset.title') }), flash ? _jsx(Text, { color: "green", children: flash }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [step.id === 'root' && _jsx(SelectList, { items: rootItems, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
167
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: scope === 'global' ? t('set.title') : t('sset.title') }), flash ? _jsx(Text, { color: "green", children: flash }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [step.id === 'root' && _jsx(SelectList, { items: rootItems, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
167
168
  setLang(code);
168
169
  ctl.setLanguage(code);
169
170
  saved();
@@ -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/Wizard.js CHANGED
@@ -2,6 +2,12 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { t } from '../i18n.js';
5
+ import { BRAND, COLOR } from './tokens.js';
6
+ function clampIndex(index, count) {
7
+ if (count <= 0)
8
+ return 0;
9
+ return Math.max(0, Math.min(index, count - 1));
10
+ }
5
11
  /**
6
12
  * Simple ↑/↓ + Entrée select list. If `allowInput` is set, the user can also
7
13
  * type a free value (e.g. a folder path or a custom model name) — typing
@@ -41,12 +47,12 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
41
47
  }
42
48
  if (key.upArrow) {
43
49
  if (!typing)
44
- setIdx((i) => (i - 1 + selectable.length) % Math.max(1, selectable.length));
50
+ setIdx((i) => clampIndex(i - 1, selectable.length));
45
51
  return;
46
52
  }
47
53
  if (key.downArrow) {
48
54
  if (!typing)
49
- setIdx((i) => (i + 1) % Math.max(1, selectable.length));
55
+ setIdx((i) => clampIndex(i + 1, selectable.length));
50
56
  return;
51
57
  }
52
58
  if (key.pageUp) {
@@ -95,9 +101,9 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
95
101
  const below = Math.max(0, items.length - start - visibleItems.length);
96
102
  return (_jsxs(Box, { flexDirection: "column", children: [above > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", above] }) : null, visibleItems.map((it, localIdx) => {
97
103
  const i = start + localIdx;
98
- return (it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeIdx ? 'cyanBright' : 'gray', bold: !typing && i === safeIdx, children: [!typing && i === safeIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i)));
99
- }), below > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", below] }) : null, allowInput && (_jsx(Box, { marginTop: items.length > 0 ? 1 : 0, children: _jsxs(Text, { color: typing ? 'cyanBright' : 'gray', children: ["\u270E", ' ', typing ? (_jsx(Text, { color: "white", children: mask ? '•'.repeat(typed.length) : typed })) : (_jsx(Text, { color: "gray", children: inputPlaceholder ?? '…' })), typing ? _jsx(Text, { color: "cyanBright", children: "\u2588" }) : null] }) }))] }));
104
+ return (it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeIdx ? COLOR.cream : 'gray', bold: !typing && i === safeIdx, children: [!typing && i === safeIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i)));
105
+ }), below > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", below] }) : null, allowInput && (_jsx(Box, { marginTop: items.length > 0 ? 1 : 0, children: _jsxs(Text, { color: typing ? COLOR.cream : 'gray', children: ["\u270E", ' ', typing ? (_jsx(Text, { color: "white", children: mask ? '•'.repeat(typed.length) : typed })) : (_jsx(Text, { color: "gray", children: inputPlaceholder ?? '…' })), typing ? _jsx(Text, { color: COLOR.cream, children: "\u2588" }) : null] }) }))] }));
100
106
  }
101
107
  export function WizardStep({ step, total, title, children, footer, }) {
102
- return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["[", step, "/", total, "] ", title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: children }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: footer ?? t('wiz.footer.select') }) })] }));
108
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: BRAND.primary, children: ["[", step, "/", total, "] ", title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: children }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: footer ?? t('wiz.footer.select') }) })] }));
103
109
  }
package/dist/ui/theme.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { t } from '../i18n.js';
2
- import { STATE } from './tokens.js';
2
+ import { COLOR, STATE } from './tokens.js';
3
3
  /** Strong visual cues: icon + label (i18n key) + color per state. */
4
4
  export const STATE_LABEL = {
5
5
  idle: { icon: '◇', labelKey: 'st.idle', color: STATE.idle },
6
6
  thinking: { icon: '🧠', labelKey: 'st.thinking', color: STATE.thinking },
7
- listening: { icon: '👂', labelKey: 'st.listening', color: 'cyanBright' },
7
+ listening: { icon: '👂', labelKey: 'st.listening', color: COLOR.cream },
8
8
  working: { icon: '🔨', labelKey: 'st.working', color: 'green' },
9
9
  waiting: { icon: '✋', labelKey: 'st.waiting', color: 'magenta' },
10
- paused: { icon: '⏸', labelKey: 'st.paused', color: 'blue' },
10
+ paused: { icon: '⏸', labelKey: 'st.paused', color: COLOR.creamMuted },
11
11
  done: { icon: '✅', labelKey: 'st.done', color: STATE.done },
12
12
  error: { icon: '✖', labelKey: 'st.error', color: 'red' },
13
13
  stopped: { icon: '⏹', labelKey: 'st.stopped', color: STATE.error },
package/dist/ui/tokens.js CHANGED
@@ -6,9 +6,14 @@ export const MARK = {
6
6
  waiting: '?',
7
7
  arrow: '▸',
8
8
  };
9
+ export const COLOR = {
10
+ cream: '#f3e7c7',
11
+ creamMuted: '#c8bfa6',
12
+ promptBackground: '#5f5963',
13
+ };
9
14
  export const UI = {
10
- brand: 'cyanBright',
11
- accent: 'cyan',
15
+ brand: COLOR.cream,
16
+ accent: COLOR.cream,
12
17
  muted: 'gray',
13
18
  text: 'white',
14
19
  ok: 'greenBright',
@@ -30,12 +35,12 @@ export const STATE_META = {
30
35
  // ─── Hub redesign tokens (Phase 0) ───────────────────────────────────────────
31
36
  /** Brand colors — logotype, borders, focus indicator. */
32
37
  export const BRAND = {
33
- primary: 'cyanBright',
34
- muted: 'cyan',
38
+ primary: COLOR.cream,
39
+ muted: COLOR.creamMuted,
35
40
  };
36
41
  /** Semantic state colors mapped to agent states. */
37
42
  export const STATE = {
38
- working: 'cyan',
43
+ working: COLOR.cream,
39
44
  thinking: 'yellow',
40
45
  listening: 'yellow',
41
46
  done: 'greenBright',
@@ -46,7 +51,7 @@ export const STATE = {
46
51
  /** Mode indicator colors. `task` is undefined — it renders no mark. */
47
52
  export const MODE = {
48
53
  ask: 'yellow',
49
- plan: 'blue',
54
+ plan: COLOR.creamMuted,
50
55
  task: undefined,
51
56
  };
52
57
  /** Chrome / UI element colors. */
package/dist/ui/views.js CHANGED
@@ -1,12 +1,38 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { Box, Text, useInput, useStdout } from 'ink';
4
4
  import * as Diff from 'diff';
5
- import { visibleCommands } from '../commands.js';
5
+ import { sortCommandsForPalette, visibleCommands } from '../commands.js';
6
6
  import { Controller } from '../controller.js';
7
7
  import { fmtCost } from '../pricing.js';
8
8
  import { STATE_LABEL, stateLabel, truncate } from './theme.js';
9
9
  import { t } from '../i18n.js';
10
+ import { BRAND, COLOR } from './tokens.js';
11
+ function clampIndex(index, count) {
12
+ if (count <= 0)
13
+ return 0;
14
+ return Math.max(0, Math.min(index, count - 1));
15
+ }
16
+ function useSelectableIndex(count, pageSize) {
17
+ const [selected, setSelected] = useState(0);
18
+ const safeSelected = clampIndex(selected, count);
19
+ useEffect(() => {
20
+ if (safeSelected !== selected)
21
+ setSelected(safeSelected);
22
+ }, [safeSelected, selected]);
23
+ const step = Math.max(1, pageSize - 1);
24
+ useInput((_input, key) => {
25
+ if (key.downArrow)
26
+ setSelected((i) => clampIndex(i + 1, count));
27
+ if (key.upArrow)
28
+ setSelected((i) => clampIndex(i - 1, count));
29
+ if (key.pageDown)
30
+ setSelected((i) => clampIndex(i + step, count));
31
+ if (key.pageUp)
32
+ setSelected((i) => clampIndex(i - step, count));
33
+ });
34
+ return safeSelected;
35
+ }
10
36
  /**
11
37
  * PgUp/PgDn window over a list — the TUI runs in the alternate screen, so
12
38
  * every long view needs its own scrolling. `anchor` decides what you see
@@ -60,7 +86,8 @@ export function BoardView({ board, bodyHeight }) {
60
86
  const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents - 5) / 2)) : 8;
61
87
  const activities = [...board.fileActivity.values()].sort((a, b) => b.ts - a.ts).slice(0, sideRows);
62
88
  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)))] }));
89
+ const warnings = board.workMapWarnings.slice(-Math.max(2, Math.min(4, sideRows)));
90
+ 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)))] }));
64
91
  }
65
92
  export function NotesView({ board, bodyHeight }) {
66
93
  const fallbackVisible = useVisibleRows(7);
@@ -78,7 +105,7 @@ export function DiffView({ board, bodyHeight }) {
78
105
  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) => {
79
106
  const patch = Diff.createPatch(c.path, c.before, c.after, '', '', { context: 2 });
80
107
  const lines = patch.split('\n').slice(4, 34);
81
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color: "cyan", children: c.path }), _jsxs(Text, { color: "gray", children: [' ', t('diff.by', { agent: c.agentName, time: new Date(c.ts).toLocaleTimeString() })] })] }), lines.map((l, i) => (_jsx(Text, { color: l.startsWith('+') ? 'green' : l.startsWith('-') ? 'red' : l.startsWith('@') ? 'cyan' : 'gray', wrap: "truncate-end", children: l || ' ' }, i))), patch.split('\n').length > 38 ? _jsx(Text, { color: "gray", children: t('diff.trunc') }) : null] }, c.id));
108
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color: BRAND.primary, children: c.path }), _jsxs(Text, { color: "gray", children: [' ', t('diff.by', { agent: c.agentName, time: new Date(c.ts).toLocaleTimeString() })] })] }), lines.map((l, i) => (_jsx(Text, { color: l.startsWith('+') ? 'green' : l.startsWith('-') ? 'red' : l.startsWith('@') ? BRAND.primary : 'gray', wrap: "truncate-end", children: l || ' ' }, i))), patch.split('\n').length > 38 ? _jsx(Text, { color: "gray", children: t('diff.trunc') }) : null] }, c.id));
82
109
  }), _jsx(Below, { n: below })] }))] }));
83
110
  }
84
111
  /** Financial view: live cost / steps / tokens per agent + session total. */
@@ -89,21 +116,21 @@ export function CostView({ board, bodyHeight }) {
89
116
  const { slice, above, below } = useScrollWindow(agents, visible, 'top');
90
117
  const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
91
118
  const unknown = agents.some((a) => a.cost === null);
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') })] }));
119
+ 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: BRAND.primary, 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') })] }));
93
120
  }
94
121
  /** Skills catalog: user-authored markdown instructions agents can load. */
95
122
  export function SkillsView({ skills, bodyHeight }) {
96
123
  const fallbackVisible = useVisibleRows(8);
97
124
  const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
98
125
  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') })] }));
126
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, 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: BRAND.primary, 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') })] }));
100
127
  }
101
128
  /** Specialists catalog: personas (role + optional pinned model). */
102
129
  export function SpecialistsView({ specialists, bodyHeight }) {
103
130
  const fallbackVisible = useVisibleRows(8);
104
131
  const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
105
132
  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') })] }));
133
+ 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: BRAND.primary, 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') })] }));
107
134
  }
108
135
  /** Saved sessions: inspect available restore points; restore via /session. */
109
136
  export function SessionsView({ projectRoot, bodyHeight }) {
@@ -117,16 +144,27 @@ export function SessionsView({ projectRoot, bodyHeight }) {
117
144
  agents: s.data.agents.length,
118
145
  }) }), _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') })] }));
119
146
  }
120
- export function HelpView({ bodyHeight }) {
147
+ export function HelpView({ bodyHeight, onSelect }) {
121
148
  // Fixed intro/highlight/footer rows consume about 12 lines inside the already-sized body.
122
149
  const fallbackVisible = useVisibleRows(16);
123
150
  const visible = bodyHeight ? Math.max(3, bodyHeight - 12) : fallbackVisible;
124
- const commands = visibleCommands();
125
- const { slice, above, below } = useScrollWindow(commands, visible, 'top');
151
+ const commands = sortCommandsForPalette(visibleCommands());
152
+ const selected = useSelectableIndex(commands.length, visible);
153
+ useInput((_input, key) => {
154
+ if (key.return)
155
+ onSelect?.(commands[selected]?.name ?? '/help');
156
+ });
157
+ const start = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, commands.length - visible));
158
+ const slice = commands.slice(start, start + visible);
159
+ const above = start;
160
+ const below = Math.max(0, commands.length - start - slice.length);
126
161
  const highlights = [
127
162
  ['Agent modes', ['/ask', '/task', '/plan']],
128
163
  ['Shell approvals', ['/approvals ask', '/approvals auto', '/approvals yolo']],
129
164
  ['Navigation', ['/focus', '/attach', '/raw', '/send']],
130
165
  ];
131
- return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: t('help.title') }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l1a') }), t('help.l1b'), _jsx(Text, { bold: true, children: t('help.l1c') }), "."] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l2a') }), t('help.l2b'), _jsx(Text, { bold: true, children: t('help.l2c') }), t('help.l2d')] }), _jsx(Text, { wrap: "truncate-end", children: t('help.l3') }), _jsx(Text, { children: " " }), highlights.map(([label, names]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: "cyan", bold: true, children: [label, ": "] }), _jsx(Text, { color: "gray", children: names.join(' ') })] }, label))), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "Keyboard: \u2191/\u2193 or PgUp/PgDn scroll \u00B7 Tab/\u2192 autocomplete \u00B7 Esc back/clear \u00B7 Ctrl+U clear \u00B7 Ctrl+V image" }), _jsx(Text, { children: " " }), _jsx(Above, { n: above }), slice.map((c) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(18) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(24) }), _jsxs(Text, { color: "gray", children: [t(c.descKey), c.aliases?.length ? ` (= ${c.aliases.join(', ')})` : ''] })] }, c.name))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.states') }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.keys') })] }));
166
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: t('help.title') }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l1a') }), t('help.l1b'), _jsx(Text, { bold: true, children: t('help.l1c') }), "."] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l2a') }), t('help.l2b'), _jsx(Text, { bold: true, children: t('help.l2c') }), t('help.l2d')] }), _jsx(Text, { wrap: "truncate-end", children: t('help.l3') }), _jsx(Text, { children: " " }), highlights.map(([label, names]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: BRAND.primary, bold: true, children: [label, ": "] }), _jsx(Text, { color: "gray", children: names.join(' ') })] }, label))), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "Keyboard: \u2191/\u2193 select \u00B7 Enter run selected \u00B7 PgUp/PgDn page \u00B7 Esc back" }), _jsx(Text, { children: " " }), _jsx(Above, { n: above }), slice.map((c, i) => {
167
+ const isSelected = start + i === selected;
168
+ return (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: isSelected ? COLOR.cream : COLOR.creamMuted, bold: true, children: [isSelected ? '› ' : ' ', c.name.padEnd(16)] }), _jsx(Text, { color: "yellow", children: c.args.padEnd(24) }), _jsxs(Text, { color: "gray", children: [t(c.descKey), c.aliases?.length ? ` (= ${c.aliases.join(', ')})` : ''] })] }, c.name));
169
+ }), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.states') }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.keys') })] }));
132
170
  }
package/dist/update.js ADDED
@@ -0,0 +1,125 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { spawn } from 'node:child_process';
5
+ import { configDir } from './config.js';
6
+ import { PACKAGE_NAME, VERSION } from './version.js';
7
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
8
+ const REMIND_LATER_MS = 24 * 60 * 60 * 1000;
9
+ export function compareVersions(a, b) {
10
+ const pa = a.replace(/^v/, '').split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0);
11
+ const pb = b.replace(/^v/, '').split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0);
12
+ const len = Math.max(pa.length, pb.length);
13
+ for (let i = 0; i < len; i++) {
14
+ const left = pa[i] ?? 0;
15
+ const right = pb[i] ?? 0;
16
+ if (left !== right)
17
+ return left > right ? 1 : -1;
18
+ }
19
+ return 0;
20
+ }
21
+ function updateStateFile() {
22
+ return path.join(configDir(), 'update.json');
23
+ }
24
+ export function readUpdateState() {
25
+ try {
26
+ const file = updateStateFile();
27
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ export function writeUpdateState(state) {
34
+ try {
35
+ fs.mkdirSync(configDir(), { recursive: true });
36
+ fs.writeFileSync(updateStateFile(), JSON.stringify(state, null, 2));
37
+ }
38
+ catch {
39
+ /* best effort */
40
+ }
41
+ }
42
+ export function shouldSkipUpdateCheck(env = process.env) {
43
+ return (env.PARALLEL_SKIP_UPDATE_CHECK === '1' ||
44
+ env.CI === 'true' ||
45
+ env.GITHUB_ACTIONS === 'true' ||
46
+ env.GITLAB_CI === 'true' ||
47
+ env.BUILDKITE === 'true');
48
+ }
49
+ export function shouldPromptForUpdate(info, state, now = Date.now()) {
50
+ if (compareVersions(info.latest, info.current) <= 0)
51
+ return false;
52
+ if (state.dismissedVersion === info.latest)
53
+ return false;
54
+ if (state.skipUntil && state.skipUntil > now)
55
+ return false;
56
+ return true;
57
+ }
58
+ export async function fetchLatestVersion(timeoutMs = 1500) {
59
+ let timeout;
60
+ try {
61
+ const controller = new AbortController();
62
+ timeout = setTimeout(() => controller.abort(), timeoutMs);
63
+ const resp = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`, {
64
+ signal: controller.signal,
65
+ });
66
+ if (!resp.ok)
67
+ return null;
68
+ const data = await resp.json();
69
+ return typeof data.version === 'string' ? data.version : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ finally {
75
+ if (timeout)
76
+ clearTimeout(timeout);
77
+ }
78
+ }
79
+ function ask(question) {
80
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
81
+ return new Promise((resolve) => {
82
+ rl.question(question, (answer) => {
83
+ rl.close();
84
+ resolve(answer.trim());
85
+ });
86
+ });
87
+ }
88
+ export async function runGlobalUpdate() {
89
+ return new Promise((resolve) => {
90
+ const child = spawn('npm', ['install', '-g', PACKAGE_NAME], { stdio: 'inherit' });
91
+ child.on('close', (code) => resolve(code ?? 1));
92
+ child.on('error', () => resolve(1));
93
+ });
94
+ }
95
+ export async function maybeRunStartupUpdate(skip = false) {
96
+ if (skip || shouldSkipUpdateCheck() || !process.stdin.isTTY || !process.stdout.isTTY)
97
+ return false;
98
+ const now = Date.now();
99
+ const state = readUpdateState();
100
+ if (state.lastCheckAt && now - state.lastCheckAt < CHECK_INTERVAL_MS && (!state.skipUntil || state.skipUntil > now)) {
101
+ return false;
102
+ }
103
+ const latest = await fetchLatestVersion();
104
+ writeUpdateState({ ...state, lastCheckAt: now });
105
+ if (!latest)
106
+ return false;
107
+ const info = { current: VERSION, latest };
108
+ if (!shouldPromptForUpdate(info, state, now))
109
+ return false;
110
+ const answer = await ask(`Update Parallel ${VERSION} -> ${latest}? [y/N] `);
111
+ if (!/^y(es)?$/i.test(answer)) {
112
+ writeUpdateState({ ...state, lastCheckAt: now, skipUntil: now + REMIND_LATER_MS });
113
+ return false;
114
+ }
115
+ console.log(`Updating Parallel via \`npm install -g ${PACKAGE_NAME}\`...\n`);
116
+ const code = await runGlobalUpdate();
117
+ if (code === 0) {
118
+ writeUpdateState({ lastCheckAt: now, dismissedVersion: latest });
119
+ console.log('\nUpdate ran successfully! Please restart Parallel.');
120
+ }
121
+ else {
122
+ console.log('\nUpdate failed. Parallel will continue with the current version.');
123
+ }
124
+ return code === 0;
125
+ }
@@ -0,0 +1,2 @@
1
+ export const PACKAGE_NAME = '@parallel-cli/parallel';
2
+ export const VERSION = '0.4.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallel-cli/parallel",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",