@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/CHANGELOG.md +57 -0
- package/README.md +43 -7
- package/dist/agents/agent.js +213 -31
- package/dist/agents/execution-policy.js +58 -0
- package/dist/agents/tools.js +83 -22
- package/dist/commands.js +67 -5
- package/dist/config.js +5 -2
- package/dist/controller.js +229 -11
- package/dist/coordination/blackboard.js +8 -7
- package/dist/diagnostics.js +209 -0
- package/dist/i18n.js +60 -4
- package/dist/index.js +31 -7
- package/dist/llm/client.js +7 -3
- package/dist/project-context.js +477 -0
- package/dist/project-index.js +186 -0
- package/dist/security.js +93 -0
- package/dist/server.js +41 -2
- package/dist/ui/AgentPanel.js +6 -2
- package/dist/ui/App.js +4 -2
- package/dist/ui/AttachApp.js +6 -3
- package/dist/ui/CommandInput.js +9 -0
- package/dist/ui/SettingsPanel.js +22 -23
- package/dist/ui/Timeline.js +3 -0
- package/dist/ui/Wizard.js +49 -21
- package/dist/ui/events.js +4 -0
- package/dist/ui/views.js +4 -2
- package/dist/update.js +3 -2
- package/dist/version.js +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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 ===
|
|
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({
|
|
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
|
-
|
|
36
|
-
|
|
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.
|
|
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.
|
|
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 &&
|
|
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"
|