@parallel-cli/parallel 0.4.8 → 0.5.0

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/Wizard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { t } from '../i18n.js';
5
5
  import { BRAND, COLOR } from './tokens.js';
@@ -8,6 +8,16 @@ function clampIndex(index, count) {
8
8
  return 0;
9
9
  return Math.max(0, Math.min(index, count - 1));
10
10
  }
11
+ export function selectableIndexes(items) {
12
+ return items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
13
+ }
14
+ export function selectListWindow(itemsLength, selectedRealIndex, maxVisible) {
15
+ const visible = Math.max(1, maxVisible);
16
+ if (itemsLength <= visible || selectedRealIndex < 0)
17
+ return { start: 0, end: Math.min(itemsLength, visible) };
18
+ const start = Math.max(0, Math.min(selectedRealIndex - Math.floor(visible / 2), itemsLength - visible));
19
+ return { start, end: start + visible };
20
+ }
11
21
  /**
12
22
  * Simple ↑/↓ + Entrée select list. If `allowInput` is set, the user can also
13
23
  * type a free value (e.g. a folder path or a custom model name) — typing
@@ -17,9 +27,25 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
17
27
  const [idx, setIdx] = useState(0);
18
28
  const [typed, setTyped] = useState('');
19
29
  const typing = allowInput && typed.length > 0;
30
+ const selectable = useMemo(() => selectableIndexes(items), [items]);
31
+ const safeLogicalIdx = clampIndex(idx, selectable.length);
32
+ const safeRealIdx = selectable.length > 0 ? selectable[safeLogicalIdx] : -1;
33
+ const maxVisible = height ? Math.max(1, height - (allowInput ? 2 : 0)) : items.length;
34
+ const window = selectListWindow(items.length, safeRealIdx, maxVisible);
35
+ const visibleItems = items.slice(window.start, window.end);
36
+ const above = window.start;
37
+ const below = Math.max(0, items.length - window.end);
38
+ const pageStep = Math.max(1, Math.floor(Math.max(1, maxVisible) / 2));
39
+ useEffect(() => {
40
+ if (safeLogicalIdx !== idx)
41
+ setIdx(safeLogicalIdx);
42
+ }, [idx, safeLogicalIdx]);
43
+ const chooseCurrent = () => {
44
+ const realIdx = selectable[safeLogicalIdx];
45
+ if (realIdx !== undefined && items[realIdx])
46
+ onSelect?.(items[realIdx].value);
47
+ };
20
48
  useInput((input, key) => {
21
- // Build selectable index list each render (cheap — items is small).
22
- const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
23
49
  if (key.escape) {
24
50
  if (typed)
25
51
  setTyped('');
@@ -27,6 +53,13 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
27
53
  onBack?.();
28
54
  return;
29
55
  }
56
+ if (key.leftArrow) {
57
+ if (typed)
58
+ setTyped('');
59
+ else
60
+ onBack?.();
61
+ return;
62
+ }
30
63
  if (key.return) {
31
64
  if (typing) {
32
65
  const v = typed.trim();
@@ -35,14 +68,19 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
35
68
  onInput?.(v);
36
69
  }
37
70
  else {
38
- const realIdx = selectable[idx];
39
- if (realIdx !== undefined && items[realIdx])
40
- onSelect?.(items[realIdx].value);
71
+ chooseCurrent();
41
72
  }
42
73
  return;
43
74
  }
75
+ if ((key.tab || key.rightArrow) && !typing) {
76
+ chooseCurrent();
77
+ return;
78
+ }
44
79
  if (key.backspace || key.delete) {
45
- setTyped((v) => v.slice(0, -1));
80
+ if (typed)
81
+ setTyped((v) => v.slice(0, -1));
82
+ else
83
+ onBack?.();
46
84
  return;
47
85
  }
48
86
  if (key.upArrow) {
@@ -57,12 +95,12 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
57
95
  }
58
96
  if (key.pageUp) {
59
97
  if (!typing)
60
- setIdx((i) => Math.max(0, i - Math.max(1, Math.floor((height ?? 8) / 2))));
98
+ setIdx((i) => clampIndex(i - pageStep, selectable.length));
61
99
  return;
62
100
  }
63
101
  if (key.pageDown) {
64
102
  if (!typing)
65
- setIdx((i) => Math.min(Math.max(0, selectable.length - 1), i + Math.max(1, Math.floor((height ?? 8) / 2))));
103
+ setIdx((i) => clampIndex(i + pageStep, selectable.length));
66
104
  return;
67
105
  }
68
106
  if (key.home) {
@@ -89,19 +127,9 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
89
127
  }
90
128
  setTyped((v) => v + input);
91
129
  });
92
- // Build a separate index map so up/down skip section headers.
93
- const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
94
- const safeIdx = selectable.length > 0 ? selectable[Math.min(idx, selectable.length - 1)] : -1;
95
- const maxVisible = height ? Math.max(1, height - (allowInput ? 2 : 0)) : items.length;
96
- const start = items.length > maxVisible && safeIdx >= 0
97
- ? Math.max(0, Math.min(safeIdx - Math.floor(maxVisible / 2), items.length - maxVisible))
98
- : 0;
99
- const visibleItems = items.slice(start, start + maxVisible);
100
- const above = start;
101
- const below = Math.max(0, items.length - start - visibleItems.length);
102
130
  return (_jsxs(Box, { flexDirection: "column", children: [above > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", above] }) : null, visibleItems.map((it, localIdx) => {
103
- const i = start + localIdx;
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)));
131
+ const i = window.start + localIdx;
132
+ 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 === safeRealIdx ? COLOR.cream : 'gray', bold: !typing && i === safeRealIdx, children: [!typing && i === safeRealIdx ? '❯ ' : ' ', 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
133
  }), 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] }) }))] }));
106
134
  }
107
135
  export function WizardStep({ step, total, title, children, footer, }) {
package/dist/ui/events.js CHANGED
@@ -28,6 +28,8 @@ function classify(log) {
28
28
  }
29
29
  if (log.kind === 'note')
30
30
  return { agentId: log.agentId, kind: 'note', label: 'note', detail: cleaned || text, ts: log.ts, seq: log.seq };
31
+ if (log.kind === 'memory')
32
+ return { agentId: log.agentId, kind: 'memory', label: 'memory', detail: cleaned || text, ts: log.ts, seq: log.seq };
31
33
  if (log.kind === 'system')
32
34
  return { agentId: log.agentId, kind: 'system', label: 'system', detail: cleaned || text, ts: log.ts, seq: log.seq };
33
35
  if (log.kind === 'llm')
@@ -118,6 +120,8 @@ function categoryFor(e) {
118
120
  return 'result';
119
121
  if (e.kind === 'note' || e.kind === 'approval' || e.kind === 'question')
120
122
  return 'coordinate';
123
+ if (e.kind === 'memory')
124
+ return 'other';
121
125
  if (e.kind === 'intent')
122
126
  return 'other';
123
127
  if (e.kind === 'file') {
package/dist/ui/views.js CHANGED
@@ -121,14 +121,16 @@ export function DiffView({ board, bodyHeight }) {
121
121
  }), _jsx(Below, { n: below })] }))] }));
122
122
  }
123
123
  /** Financial view: live cost / steps / tokens per agent + session total. */
124
- export function CostView({ board, bodyHeight }) {
124
+ export function CostView({ ctl, bodyHeight }) {
125
+ const board = ctl.board;
125
126
  const agents = [...board.agents.values()];
126
127
  const fallbackVisible = useVisibleRows(8);
127
128
  const visible = bodyHeight ? Math.max(3, bodyHeight - 7) : fallbackVisible;
128
129
  const { slice, above, below } = useScrollWindow(agents, visible, 'top');
129
130
  const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
131
+ const memory = ctl.projectContextStatus();
130
132
  const unknown = agents.some((a) => a.cost === null);
131
- 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') })] }));
133
+ 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, { color: "gray", children: [(a.profile ?? 'standard').padEnd(8), " "] }), _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] })] })), _jsxs(Text, { children: [' ', t('cost.memory'), " ", _jsx(Text, { color: BRAND.primary, children: memory.model ?? '—' }), ' ', _jsx(Text, { color: "greenBright", children: memory.cost === null ? '$—' : fmtCost(memory.cost) }), ' ', _jsxs(Text, { color: "gray", children: ["(", memory.status, ", ", memory.tokensIn + memory.tokensOut, " tokens)"] })] }), _jsx(Text, { color: "gray", children: t('cost.hint') })] }));
132
134
  }
133
135
  /** Skills catalog: user-authored markdown instructions agents can load. */
134
136
  export function SkillsView({ skills, bodyHeight }) {
package/dist/update.js CHANGED
@@ -4,6 +4,7 @@ import readline from 'node:readline';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { configDir } from './config.js';
6
6
  import { PACKAGE_NAME, VERSION } from './version.js';
7
+ import { ensurePrivateDir, writeJsonAtomicPrivate } from './security.js';
7
8
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
8
9
  const REMIND_LATER_MS = 24 * 60 * 60 * 1000;
9
10
  export function compareVersions(a, b) {
@@ -32,8 +33,8 @@ export function readUpdateState() {
32
33
  }
33
34
  export function writeUpdateState(state) {
34
35
  try {
35
- fs.mkdirSync(configDir(), { recursive: true });
36
- fs.writeFileSync(updateStateFile(), JSON.stringify(state, null, 2));
36
+ ensurePrivateDir(configDir());
37
+ writeJsonAtomicPrivate(updateStateFile(), state);
37
38
  }
38
39
  catch {
39
40
  /* best effort */
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const PACKAGE_NAME = '@parallel-cli/parallel';
2
- export const VERSION = '0.4.7';
2
+ export const VERSION = '0.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallel-cli/parallel",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "Real-time coding agents that work like a live team on one shared repository.",
5
5
  "keywords": [
6
6
  "cli",
@@ -48,7 +48,7 @@
48
48
  "scripts": {
49
49
  "build": "tsc && chmod +x dist/index.js",
50
50
  "dev": "tsc --watch",
51
- "test": "npm run build && ( [ -d test ] && node --test test/*.test.mjs && npm run test:pty || echo 'test/ directory not found, skipping tests' )",
51
+ "test": "npm run build && if [ -d test ]; then node --test test/*.test.mjs && npm run test:pty; else echo 'test/ directory not found, skipping tests'; fi",
52
52
  "test:pty": "sh test/pty.sh",
53
53
  "start": "node dist/index.js",
54
54
  "prepublishOnly": "npm run build"