@parallel-cli/parallel 0.4.5 → 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/CHANGELOG.md +25 -0
- package/README.md +22 -4
- package/dist/commands.js +31 -0
- package/dist/controller.js +1 -1
- package/dist/i18n.js +16 -0
- package/dist/index.js +9 -0
- package/dist/ui/AgentPanel.js +4 -8
- package/dist/ui/App.js +20 -23
- package/dist/ui/AttachApp.js +2 -2
- package/dist/ui/CommandInput.js +86 -40
- package/dist/ui/Md.js +4 -3
- package/dist/ui/SettingsPanel.js +2 -1
- package/dist/ui/Wizard.js +11 -5
- package/dist/ui/theme.js +3 -3
- package/dist/ui/tokens.js +11 -6
- package/dist/ui/views.js +47 -10
- package/dist/update.js +125 -0
- package/dist/version.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Parallel are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.4.6 - 2026-06-24
|
|
6
|
+
|
|
7
|
+
### 0.4.6 Added
|
|
8
|
+
|
|
9
|
+
- Added an interactive npm update prompt on startup with daily cache, CI/headless/attach skips, `PARALLEL_SKIP_UPDATE_CHECK=1`, and `--no-update`.
|
|
10
|
+
- Added a Codex-like empty hub with a quieter framed header, cream-toned accents, and a full-width prompt block with distinct background.
|
|
11
|
+
- Added a complete paginated slash palette driven by one deterministic rendered order.
|
|
12
|
+
- Added selectable `/help` command navigation with visible highlight and Enter-to-run behavior.
|
|
13
|
+
- Added localized prompt placeholder copy and an i18n audit to keep all used UI keys translated.
|
|
14
|
+
|
|
15
|
+
### 0.4.6 Changed
|
|
16
|
+
|
|
17
|
+
- Reduced default Hub noise by removing persistent startup toasts, duplicate task hints, and always-on command footer lines.
|
|
18
|
+
- Replaced blue/cyan UI accents with a softer cream theme across hub, palettes, wizard/settings lists, markdown summaries, and attached agent terminals.
|
|
19
|
+
- Made the prompt block start at three rows, grow when input wraps, and show the blinking cursor on the first placeholder character while empty.
|
|
20
|
+
- Reused the same minimal prompt treatment in dedicated agent terminals and toned down their footer.
|
|
21
|
+
- Simplified agent rows so secondary telemetry only appears when there is a useful latest signal or result.
|
|
22
|
+
- Moved command palette priority to shared command helpers so autocomplete, help, and rendering stay aligned.
|
|
23
|
+
|
|
24
|
+
### 0.4.6 Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed slash palette Up/Down navigation jumping to visually unrelated commands.
|
|
27
|
+
- Fixed `/help` being scrollable but not actually selectable.
|
|
28
|
+
- Fixed Wizard and Settings selection lists wrapping around unexpectedly by clamping navigation at list boundaries.
|
|
29
|
+
|
|
5
30
|
## 0.4.5 - 2026-06-23
|
|
6
31
|
|
|
7
32
|
### 0.4.5 Added
|
package/README.md
CHANGED
|
@@ -19,13 +19,14 @@ Parallel lets you run several AI coding agents on the same repository at the sam
|
|
|
19
19
|
- Use context-aware input in the hub, focus view, and attached agent terminals.
|
|
20
20
|
- Steer one agent with `@a1 ...` or broadcast with `@all ...`.
|
|
21
21
|
- Open dedicated agent terminals with native scrollback.
|
|
22
|
-
-
|
|
22
|
+
- Use a cleaner Codex-like hub with a framed header, focused prompt bar, and quieter empty state.
|
|
23
23
|
- Review agents, notes, file activity, diffs, cost, skills, specialists, and saved sessions from the TUI.
|
|
24
24
|
- Track shell-created file mutations in the same live diff feed as agent edits.
|
|
25
25
|
- Configure OpenAI-compatible providers through a guided wizard and settings panel.
|
|
26
26
|
- Use 29 provider presets across Western, Chinese, Gateway, Inference, and Local categories.
|
|
27
27
|
- Support local no-key endpoints such as Ollama and vLLM/SGLang.
|
|
28
28
|
- Keep shell execution controlled with `ask`, `auto-safe`, or `yolo` approvals.
|
|
29
|
+
- Get prompted for npm updates at startup, with an explicit skip path.
|
|
29
30
|
- Save and restore project sessions.
|
|
30
31
|
- Run headless multi-agent jobs for CI or scripts.
|
|
31
32
|
|
|
@@ -117,7 +118,9 @@ Plain text is equivalent to `/task`.
|
|
|
117
118
|
|
|
118
119
|
## Control Room
|
|
119
120
|
|
|
120
|
-
The main TUI is the Parallel hub.
|
|
121
|
+
The main TUI is the Parallel hub. The default view stays intentionally quiet: a Codex-like framed header, cream-toned accents, a focused prompt block, and detailed status moved into explicit views.
|
|
122
|
+
|
|
123
|
+
It is designed to answer:
|
|
121
124
|
|
|
122
125
|
- what needs your input
|
|
123
126
|
- which agents are working
|
|
@@ -129,7 +132,7 @@ Input has three explicit contexts:
|
|
|
129
132
|
|
|
130
133
|
- Hub: plain text launches a new `/task` agent. Slash suggestions show hub commands and agent arguments autocomplete for `/focus`, `/send`, `/attach`, `/pause`, `/resume`, `/stop`, `/restore`, and `/commit`.
|
|
131
134
|
- Focus: after `/focus a1`, plain text talks to the focused agent instead of spawning a new one. `/raw` affects this view only.
|
|
132
|
-
- Attach: in `parallel attach a1`,
|
|
135
|
+
- Attach: in `parallel attach a1`, the same minimal prompt UI steers the attached agent. `/task`, `/ask`, and `/plan` spawn new agents from that terminal, while `@all ...`, `@a2 ...`, and `/send ...` route instructions through the main session.
|
|
133
136
|
|
|
134
137
|
Use `Name: task` when naming an agent:
|
|
135
138
|
|
|
@@ -157,9 +160,11 @@ Commands are typed in the control room input. When a long view is open, use Esca
|
|
|
157
160
|
Keyboard behavior:
|
|
158
161
|
|
|
159
162
|
- `/` opens slash command suggestions.
|
|
160
|
-
- Up/Down selects suggestions
|
|
163
|
+
- Up/Down selects suggestions in the same order they appear.
|
|
161
164
|
- Enter accepts the selected suggestion.
|
|
162
165
|
- Tab or Right accepts the best completion.
|
|
166
|
+
- `/help` is keyboard navigable: Up/Down moves the visible selection, PgUp/PgDn pages, and Enter runs the selected command.
|
|
167
|
+
- Wizard, settings, slash suggestions, and help views use clamped keyboard selection so the highlight does not jump, disappear, or wrap unexpectedly.
|
|
163
168
|
- PgUp/PgDn scrolls the hub or focus view even while the input is active. Up/Down scrolls long views and navigates suggestions/history.
|
|
164
169
|
- Escape returns to the agents view or clears the input.
|
|
165
170
|
|
|
@@ -239,6 +244,19 @@ Environment variables:
|
|
|
239
244
|
- `PARALLEL_BASE_URL`: override the default provider base URL.
|
|
240
245
|
- `PARALLEL_MODEL`: override the session model.
|
|
241
246
|
- `PARALLEL_NO_ALT_SCREEN=1`: disable the alternate terminal screen.
|
|
247
|
+
- `PARALLEL_SKIP_UPDATE_CHECK=1`: disable npm update checks.
|
|
248
|
+
|
|
249
|
+
## Updates
|
|
250
|
+
|
|
251
|
+
On interactive startup, Parallel checks npm at most once per day for a newer `@parallel-cli/parallel` version. The check is skipped in `attach`, `--headless`, `--first-run`, CI, non-TTY sessions, or when `PARALLEL_SKIP_UPDATE_CHECK=1` is set.
|
|
252
|
+
|
|
253
|
+
When an update is available, Parallel asks before running:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
npm install -g @parallel-cli/parallel
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
If the update succeeds, restart Parallel to run the new version. Use `parallel --no-update` for a one-off launch without checking.
|
|
242
260
|
|
|
243
261
|
## Commands
|
|
244
262
|
|
package/dist/commands.js
CHANGED
|
@@ -60,12 +60,43 @@ export const COMMANDS = [
|
|
|
60
60
|
export function visibleCommands() {
|
|
61
61
|
return COMMANDS.filter((c) => !c.hidden);
|
|
62
62
|
}
|
|
63
|
+
const COMMAND_GROUP_ORDER = ['modes', 'control', 'views', 'settings', 'git', 'other'];
|
|
64
|
+
const COMMAND_PALETTE_PRIORITY = [
|
|
65
|
+
'/ask',
|
|
66
|
+
'/task',
|
|
67
|
+
'/plan',
|
|
68
|
+
'/send',
|
|
69
|
+
'/focus',
|
|
70
|
+
'/attach',
|
|
71
|
+
'/agents',
|
|
72
|
+
'/board',
|
|
73
|
+
'/diff',
|
|
74
|
+
'/settings',
|
|
75
|
+
'/help',
|
|
76
|
+
'/quit',
|
|
77
|
+
];
|
|
78
|
+
function commandRank(c) {
|
|
79
|
+
const priority = COMMAND_PALETTE_PRIORITY.indexOf(c.name);
|
|
80
|
+
if (priority !== -1)
|
|
81
|
+
return priority;
|
|
82
|
+
const group = COMMAND_GROUP_ORDER.indexOf(c.group ?? 'other');
|
|
83
|
+
return COMMAND_PALETTE_PRIORITY.length + group * 100 + COMMANDS.indexOf(c);
|
|
84
|
+
}
|
|
85
|
+
export function sortCommandsForPalette(commands) {
|
|
86
|
+
return [...commands].sort((a, b) => commandRank(a) - commandRank(b) || a.name.localeCompare(b.name));
|
|
87
|
+
}
|
|
63
88
|
export function matchCommands(input, opts = {}) {
|
|
64
89
|
if (!input.startsWith('/'))
|
|
65
90
|
return [];
|
|
66
91
|
const word = input.split(/\s+/)[0].toLowerCase();
|
|
67
92
|
return COMMANDS.filter((c) => opts.includeHidden || !c.hidden).filter((c) => c.name.startsWith(word) || c.aliases?.some((a) => a.startsWith(word)));
|
|
68
93
|
}
|
|
94
|
+
export function commandPalette(input, opts = {}) {
|
|
95
|
+
const allowed = opts.allowedNames
|
|
96
|
+
? (c) => opts.allowedNames.includes(c.name) || c.aliases?.some((a) => opts.allowedNames.includes(a))
|
|
97
|
+
: () => true;
|
|
98
|
+
return sortCommandsForPalette(matchCommands(input, opts).filter(allowed));
|
|
99
|
+
}
|
|
69
100
|
function agentList(ctl) {
|
|
70
101
|
return [...ctl.board.agents.values()].map((a) => a.name).join(', ') || t('m.none');
|
|
71
102
|
}
|
package/dist/controller.js
CHANGED
|
@@ -9,7 +9,7 @@ import { saveConfig, getProvider, upsertProvider } from './config.js';
|
|
|
9
9
|
import { priceFor, fmtCost } from './pricing.js';
|
|
10
10
|
import { loadSkills, loadSpecialists } from './skills.js';
|
|
11
11
|
import { t } from './i18n.js';
|
|
12
|
-
const AGENT_COLORS = ['
|
|
12
|
+
const AGENT_COLORS = ['#f3e7c7', 'magenta', 'yellow', 'green', 'whiteBright', 'redBright', '#c8bfa6', 'magentaBright'];
|
|
13
13
|
export function normalizeShellApprovalMode(mode) {
|
|
14
14
|
if (mode === 'ask' || mode === 'auto-safe' || mode === 'yolo')
|
|
15
15
|
return mode;
|
package/dist/i18n.js
CHANGED
|
@@ -76,6 +76,10 @@ const en = {
|
|
|
76
76
|
'main.ready1': '⚡ Ready — folder: {folder}',
|
|
77
77
|
'main.ready2': 'Type a task + Enter to launch your first agent. /help for help.',
|
|
78
78
|
'main.empty': 'No agents yet. Type a task + Enter to launch your first agent — then launch more at any time, even while they work.',
|
|
79
|
+
'main.prompt': 'Example: Redesign the UI',
|
|
80
|
+
'main.emptyCard.tagline': 'Multi-agent coding from one terminal.',
|
|
81
|
+
'main.emptyCard.cta': 'Describe work below to launch the first agent.',
|
|
82
|
+
'main.emptyCard.hints': '/ for commands · @agent to steer · /help for shortcuts',
|
|
79
83
|
'main.status': 'Enter = new agent N+1 (even while others work) · @Name = real-time instruction · /help · views: /agents /board /diff /notes',
|
|
80
84
|
'main.placeholder': 'Type a task (= new agent N+1) · @Agent message · /command',
|
|
81
85
|
'agent.summary': 'Summary',
|
|
@@ -454,6 +458,10 @@ const fr = {
|
|
|
454
458
|
'main.ready1': '⚡ Prêt — dossier : {folder}',
|
|
455
459
|
'main.ready2': "Tape une tâche + Entrée pour lancer ton premier agent. /help pour l'aide.",
|
|
456
460
|
'main.empty': "Aucun agent pour le moment. Tape une tâche + Entrée pour lancer ton premier agent — puis relances-en d'autres à tout moment, même pendant qu'ils travaillent.",
|
|
461
|
+
'main.prompt': 'Exemple : Fais moi une refonte UI',
|
|
462
|
+
'main.emptyCard.tagline': 'Code multi-agent depuis un terminal.',
|
|
463
|
+
'main.emptyCard.cta': 'Décris le travail ci-dessous pour lancer le premier agent.',
|
|
464
|
+
'main.emptyCard.hints': '/ pour les commandes · @agent pour piloter · /help pour les raccourcis',
|
|
457
465
|
'main.status': 'Entrée = nouvel agent N+1 (même pendant que les autres travaillent) · @Nom = instruction temps réel · /help · vues : /agents /board /diff /notes',
|
|
458
466
|
'main.placeholder': 'Tape une tâche (= nouvel agent N+1) · @Agent message · /commande',
|
|
459
467
|
'agent.summary': 'Récapitulatif',
|
|
@@ -820,6 +828,10 @@ const es = {
|
|
|
820
828
|
'main.ready1': '⚡ Listo — carpeta: {folder}',
|
|
821
829
|
'main.ready2': 'Escribe una tarea + Enter para lanzar tu primer agente. /help para ayuda.',
|
|
822
830
|
'main.empty': 'Aún no hay agentes. Escribe una tarea + Enter para lanzar tu primer agente — luego lanza más en cualquier momento, incluso mientras trabajan.',
|
|
831
|
+
'main.prompt': 'Ejemplo: hazme un rediseño de UI',
|
|
832
|
+
'main.emptyCard.tagline': 'Código multiagente desde un terminal.',
|
|
833
|
+
'main.emptyCard.cta': 'Describe el trabajo abajo para lanzar el primer agente.',
|
|
834
|
+
'main.emptyCard.hints': '/ para comandos · @agente para dirigir · /help para atajos',
|
|
823
835
|
'main.status': 'Enter = nuevo agente N+1 (incluso mientras otros trabajan) · @Nombre = instrucción en tiempo real · /help · vistas: /agents /board /diff /notes',
|
|
824
836
|
'main.placeholder': 'Escribe una tarea (= nuevo agente N+1) · @Agente mensaje · /comando',
|
|
825
837
|
'agent.summary': 'Resumen',
|
|
@@ -1186,6 +1198,10 @@ const zh = {
|
|
|
1186
1198
|
'main.ready1': '⚡ 就绪 — 文件夹:{folder}',
|
|
1187
1199
|
'main.ready2': '输入任务 + 回车即可启动第一个智能体。/help 查看帮助。',
|
|
1188
1200
|
'main.empty': '尚无智能体。输入任务 + 回车启动第一个 — 之后可随时启动更多,即使它们正在工作。',
|
|
1201
|
+
'main.prompt': '示例:帮我重做 UI',
|
|
1202
|
+
'main.emptyCard.tagline': '在一个终端中进行多智能体编码。',
|
|
1203
|
+
'main.emptyCard.cta': '在下方描述工作以启动第一个智能体。',
|
|
1204
|
+
'main.emptyCard.hints': '/ 查看命令 · @智能体 可指挥 · /help 查看快捷键',
|
|
1189
1205
|
'main.status': '回车 = 新智能体 N+1(即使其他智能体正在工作)· @名称 = 实时指令 · /help · 视图:/agents /board /diff /notes',
|
|
1190
1206
|
'main.placeholder': '输入任务(= 新智能体 N+1)· @智能体 消息 · /命令',
|
|
1191
1207
|
'agent.summary': '摘要',
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { App } from './ui/App.js';
|
|
|
8
8
|
import { Controller } from './controller.js';
|
|
9
9
|
import { loadConfig, providerReady, setConfigHome } from './config.js';
|
|
10
10
|
import { setLang } from './i18n.js';
|
|
11
|
+
import { maybeRunStartupUpdate } from './update.js';
|
|
11
12
|
const argv = process.argv.slice(2);
|
|
12
13
|
function takeFlagValue(flag) {
|
|
13
14
|
const i = argv.indexOf(flag);
|
|
@@ -26,6 +27,9 @@ if (headless)
|
|
|
26
27
|
const jsonOut = argv.includes('--json');
|
|
27
28
|
if (jsonOut)
|
|
28
29
|
argv.splice(argv.indexOf('--json'), 1);
|
|
30
|
+
const noUpdate = argv.includes('--no-update');
|
|
31
|
+
if (noUpdate)
|
|
32
|
+
argv.splice(argv.indexOf('--no-update'), 1);
|
|
29
33
|
const configHome = takeFlagValue('--config-home');
|
|
30
34
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
31
35
|
console.log(`⚡ Parallel — real-time parallel coding agents.
|
|
@@ -39,6 +43,8 @@ Usage:
|
|
|
39
43
|
parallel --first-run Test the first-run wizard with a temporary config home
|
|
40
44
|
parallel --config-home <dir> [folder]
|
|
41
45
|
Use <dir>/config.json instead of ~/.parallel/config.json
|
|
46
|
+
parallel --no-update [folder]
|
|
47
|
+
Start without checking npm for a newer Parallel version
|
|
42
48
|
parallel --headless "task1" ["task2"…] [--json]
|
|
43
49
|
No TUI: one agent per task in the current folder,
|
|
44
50
|
auto-approved commands, summary (or JSON) on stdout — for CI
|
|
@@ -48,6 +54,7 @@ Environment variables:
|
|
|
48
54
|
PARALLEL_MODEL Default model
|
|
49
55
|
PARALLEL_BASE_URL OpenAI-compatible endpoint
|
|
50
56
|
PARALLEL_NO_ALT_SCREEN=1 Disable the alternate terminal screen.
|
|
57
|
+
PARALLEL_SKIP_UPDATE_CHECK=1 Disable npm update checks.
|
|
51
58
|
|
|
52
59
|
Inside the TUI:
|
|
53
60
|
<task> + Enter Launch agent N+1 — even while the others are working
|
|
@@ -172,6 +179,8 @@ if (!process.stdout.isTTY) {
|
|
|
172
179
|
console.error('Parallel requires an interactive terminal (TTY).');
|
|
173
180
|
process.exit(1);
|
|
174
181
|
}
|
|
182
|
+
if (await maybeRunStartupUpdate(firstRun || noUpdate))
|
|
183
|
+
process.exit(0);
|
|
175
184
|
const config = loadConfig();
|
|
176
185
|
if (config.language)
|
|
177
186
|
setLang(config.language);
|
package/dist/ui/AgentPanel.js
CHANGED
|
@@ -6,7 +6,7 @@ import { elapsed, truncate } from './theme.js';
|
|
|
6
6
|
import { Md } from './Md.js';
|
|
7
7
|
import { Spinner } from './Spinner.js';
|
|
8
8
|
import { Timeline } from './Timeline.js';
|
|
9
|
-
import { MARK, MODE, STATE_META, UI, ANIM } from './tokens.js';
|
|
9
|
+
import { MARK, MODE, STATE_META, UI, ANIM, COLOR } from './tokens.js';
|
|
10
10
|
import { latestSignal, toUIEvents } from './events.js';
|
|
11
11
|
export const KIND_COLOR = {
|
|
12
12
|
tool: UI.accent,
|
|
@@ -52,7 +52,7 @@ function ResultBlock({ agent, compact = false }) {
|
|
|
52
52
|
const SPINNER_STATES = new Set(['thinking', 'working', 'listening', 'waiting']);
|
|
53
53
|
function spinnerColor(state) {
|
|
54
54
|
if (state === 'working')
|
|
55
|
-
return
|
|
55
|
+
return COLOR.cream;
|
|
56
56
|
return 'yellow'; // thinking, listening, waiting
|
|
57
57
|
}
|
|
58
58
|
function modeChar(mode) {
|
|
@@ -87,18 +87,14 @@ export function AgentRow({ agent, logs, cols, }) {
|
|
|
87
87
|
const telemetry = formatAgentTelemetry(agent);
|
|
88
88
|
const signal = latestSignal(agent, toUIEvents(logs));
|
|
89
89
|
const specialist = agent.specialist ? ` #${agent.specialist}` : '';
|
|
90
|
-
// Line 2 content
|
|
91
90
|
let line2 = null;
|
|
92
91
|
if (agent.lastResult) {
|
|
93
92
|
line2 = { text: `✓ ${compactResultSummary(agent.lastResult, line2Max)}`, color: UI.ok };
|
|
94
93
|
}
|
|
95
|
-
else if (signal) {
|
|
94
|
+
else if (signal && signal !== agent.task) {
|
|
96
95
|
line2 = { text: `▸ ${truncate(signal, line2Max)}`, color: UI.accent };
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
line2 = { text: meta.label, color: meta.color };
|
|
100
|
-
}
|
|
101
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [SPINNER_STATES.has(agent.state) ? (_jsx(Spinner, { color: pulseColor ?? spinnerColor(agent.state) })) : (_jsx(Text, { color: pulseColor ?? meta.color, bold: true, children: meta.mark })), _jsx(Text, { children: " " }), _jsx(Text, { color: agent.color, bold: true, children: name }), mode ? (_jsxs(Text, { color: mode.color, children: [" ", mode.char] })) : null, specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: line2.color, wrap: "truncate-end", children: line2.text }), _jsx(Text, { color: UI.muted, children: telemetry })] })] }));
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [SPINNER_STATES.has(agent.state) ? (_jsx(Spinner, { color: pulseColor ?? spinnerColor(agent.state) })) : (_jsx(Text, { color: pulseColor ?? meta.color, bold: true, children: meta.mark })), _jsx(Text, { children: " " }), _jsx(Text, { color: agent.color, bold: true, children: name }), mode ? (_jsxs(Text, { color: mode.color, children: [" ", mode.char] })) : null, specialist ? _jsx(Text, { color: UI.note, children: specialist }) : null, _jsxs(Text, { color: UI.text, children: [" ", truncate(agent.task, taskMax)] })] }), line2 ? (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: line2.color, wrap: "truncate-end", children: line2.text }), _jsx(Text, { color: UI.muted, children: telemetry })] })) : null] }));
|
|
102
98
|
}
|
|
103
99
|
export function AgentTranscript({ agent, logs, raw = false, scrolled = 0, cols = 100, }) {
|
|
104
100
|
const meta = STATE_META[agent.state];
|
package/dist/ui/App.js
CHANGED
|
@@ -16,9 +16,8 @@ import { SettingsPanel } from './SettingsPanel.js';
|
|
|
16
16
|
import { BoardView, CostView, DiffView, HelpView, NotesView, SessionsView, SkillsView, SpecialistsView } from './views.js';
|
|
17
17
|
import { SelectList, WizardStep } from './Wizard.js';
|
|
18
18
|
import { BRAND, CHROME, STATE, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
19
|
+
import { VERSION } from '../version.js';
|
|
19
20
|
const LOGO = 'Parallel';
|
|
20
|
-
// Version from package.json. Hardcoded — rootDir: "src" prevents importing ../../package.json.
|
|
21
|
-
const VERSION = '0.4.5';
|
|
22
21
|
function usableProvider(config) {
|
|
23
22
|
const p = getProvider(config);
|
|
24
23
|
return p && providerReady(p) && (p.defaultModel || p.models[0]) ? p : undefined;
|
|
@@ -65,12 +64,7 @@ export function App({ config, initialFolder }) {
|
|
|
65
64
|
// Focus mode (/focus <agent>): plain input is routed to that agent.
|
|
66
65
|
const [focus, setFocus] = useState(null);
|
|
67
66
|
const [rawLogs, setRawLogs] = useState(false);
|
|
68
|
-
const [systemLines, setSystemLines] = useState(
|
|
69
|
-
? [
|
|
70
|
-
{ text: t('main.ready1', { folder: directFolder }), level: 'ok' },
|
|
71
|
-
{ text: t('main.ready2'), level: 'info' },
|
|
72
|
-
]
|
|
73
|
-
: []);
|
|
67
|
+
const [systemLines, setSystemLines] = useState([]);
|
|
74
68
|
const [inputReady, setInputReady] = useState(Boolean(directFolder));
|
|
75
69
|
const ctl = ctlRef.current;
|
|
76
70
|
const leaveCurrentProject = () => {
|
|
@@ -277,10 +271,7 @@ export function App({ config, initialFolder }) {
|
|
|
277
271
|
enterMain();
|
|
278
272
|
};
|
|
279
273
|
const enterMain = () => {
|
|
280
|
-
setSystemLines([
|
|
281
|
-
{ text: t('main.ready1', { folder }), level: 'ok' },
|
|
282
|
-
{ text: t('main.ready2'), level: 'info' },
|
|
283
|
-
]);
|
|
274
|
+
setSystemLines([]);
|
|
284
275
|
setPhase('main');
|
|
285
276
|
setInputReady(false);
|
|
286
277
|
setTimeout(() => setInputReady(true), 350);
|
|
@@ -311,7 +302,7 @@ export function App({ config, initialFolder }) {
|
|
|
311
302
|
if (phase !== 'main') {
|
|
312
303
|
const totalSteps = 5;
|
|
313
304
|
const sessionProvider = ctl ? ctl.sessionProvider() : getProvider(config);
|
|
314
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color:
|
|
305
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: LOGO }), _jsx(Text, { color: "gray", children: t('tagline') }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === 'lang' && (_jsx(WizardStep, { step: 1, total: totalSteps, title: t('wiz.lang.title'), children: _jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: wizardListHeight, onSelect: chooseLang }) })), phase === 'folder' && (_jsxs(WizardStep, { step: 2, total: totalSteps, title: t('wiz.folder.title'), footer: t('wiz.folder.footer'), children: [wizardError ? _jsx(Text, { color: "red", children: wizardError }) : null, _jsx(SelectList, { items: [
|
|
315
306
|
{ label: process.cwd(), value: process.cwd(), hint: t('wiz.folder.current') },
|
|
316
307
|
...config.recentFolders
|
|
317
308
|
.filter((f) => f !== process.cwd())
|
|
@@ -523,6 +514,10 @@ export function App({ config, initialFolder }) {
|
|
|
523
514
|
}
|
|
524
515
|
}, notify: ui.system }));
|
|
525
516
|
}
|
|
517
|
+
function EmptyHub({ bodyHeight }) {
|
|
518
|
+
const topPad = Math.max(0, Math.floor((bodyHeight - 3) / 2));
|
|
519
|
+
return (_jsxs(Box, { flexDirection: "column", children: [topPad > 0 ? _jsx(Box, { height: topPad }) : null, _jsx(Text, { color: UI.text, children: t('main.emptyCard.cta') }), _jsx(Text, { color: CHROME.muted, children: t('main.emptyCard.hints') })] }));
|
|
520
|
+
}
|
|
526
521
|
function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames, approval, question, inputActive, onInput, onEscape, notify, }) {
|
|
527
522
|
const agents = [...ctl.board.agents.values()];
|
|
528
523
|
// Adapt the layout to the REAL terminal size (never resize the user's terminal).
|
|
@@ -530,11 +525,10 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
530
525
|
const cols = Math.max(20, stdout?.columns ?? 100);
|
|
531
526
|
const rows = Math.max(12, stdout?.rows ?? 30);
|
|
532
527
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
528
|
+
const [inputRows, setInputRows] = useState(3);
|
|
533
529
|
// Height budget: fixed sections → body gets the remainder.
|
|
534
|
-
const headerLines = 4; //
|
|
535
|
-
const
|
|
536
|
-
const footerLine1 = agents.length === 0 ? 1 : 0;
|
|
537
|
-
const footerLines = footerLine1 + footerLine2;
|
|
530
|
+
const headerLines = 4; // Codex-like framed header: top + 2 content lines + bottom
|
|
531
|
+
const footerLines = 1;
|
|
538
532
|
// System messages: count actual rendered lines (including \n splits + "Session" label).
|
|
539
533
|
const systemMsgLines = systemLines.length > 0 && !settingsOpen
|
|
540
534
|
? (agents.length > 0 ? 1 : 0) + // "Session" label
|
|
@@ -544,8 +538,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
544
538
|
.slice(-2)
|
|
545
539
|
: systemLines).reduce((sum, l) => sum + l.text.split('\n').length, 0)
|
|
546
540
|
: 0;
|
|
547
|
-
const inputLines =
|
|
548
|
-
const spacerLines =
|
|
541
|
+
const inputLines = inputRows;
|
|
542
|
+
const spacerLines = 1;
|
|
549
543
|
const approvalHeight = approval ? 6 : 0;
|
|
550
544
|
const questionHeight = question ? 7 : 0;
|
|
551
545
|
const bodyHeight = Math.max(1, rows - headerLines - footerLines - systemMsgLines - inputLines - spacerLines - approvalHeight - questionHeight);
|
|
@@ -630,6 +624,9 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
630
624
|
: agents.some((a) => ['waiting', 'paused'].includes(a.state)) ? 'yellow'
|
|
631
625
|
: 'gray';
|
|
632
626
|
const folderMax = Math.max(10, cols - 40);
|
|
627
|
+
const provider = ctl.sessionProvider();
|
|
628
|
+
const providerModel = provider ? `${provider.name}:${ctl.session.model}` : 'no model';
|
|
629
|
+
const headerColor = view === 'agents' && agents.length === 0 ? BRAND.muted : CHROME.muted;
|
|
633
630
|
// View breadcrumb: when not in agents view, show the view name instead of "control room".
|
|
634
631
|
const VIEW_LABEL = {
|
|
635
632
|
agents: 'control room',
|
|
@@ -645,7 +642,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
645
642
|
specialists: 'specialists',
|
|
646
643
|
};
|
|
647
644
|
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
648
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color:
|
|
645
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: headerColor, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", agents.length === 0 ? 'ready' : viewLabel] }), rawLogs && focused ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: headerColor, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : (_jsx(Text, { color: CHROME.muted, children: providerModel })), _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: headerColor, children: " \u2502" })] }), _jsxs(Text, { color: headerColor, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight, onSelect: (cmd) => onInput(cmd) })) : agents.length === 0 ? (_jsx(EmptyHub, { bodyHeight: bodyHeight })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll, cols: cols }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
649
646
|
? systemLines
|
|
650
647
|
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
651
648
|
.slice(-2)
|
|
@@ -657,9 +654,9 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
657
654
|
// Split on \n so multiline i18n messages render correctly (Ink <Text> doesn't interpret \n).
|
|
658
655
|
const lines = l.text.split('\n');
|
|
659
656
|
return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
|
|
660
|
-
})] })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(CommandInput, { active: inputActive, placeholder: focus ? `Message ${focus} or /command` : '
|
|
661
|
-
|
|
662
|
-
|
|
657
|
+
})] })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(CommandInput, { active: inputActive, placeholder: focus ? `Message ${focus} or /command` : t('main.prompt'), context: focus ? 'focus' : 'hub', targetAgent: focused?.name, modelLabel: ctl.sessionProvider() ? `${ctl.sessionProvider()?.name}:${ctl.session.model}` : undefined, agentNames: agentNames, agents: agents, width: cols, onHeightChange: setInputRows, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: [_jsx(Text, { color: CHROME.muted, children: "/ for commands" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Shell " }), _jsx(Text, { color: ctl.session.approvalMode === 'ask' ? UI.warn :
|
|
658
|
+
ctl.session.approvalMode === 'yolo' ? UI.danger :
|
|
659
|
+
UI.ok, children: ctl.session.approvalMode === 'auto-safe' ? 'auto' : ctl.session.approvalMode }), agents.length > 0 ? _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 Sessions ", Controller.listSessions(ctl.projectRoot).length] }) : null, ctl.questions.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u2753", ctl.questions.length] })) : null, ctl.approvals.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u23F3", ctl.approvals.length] })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] }) })] }));
|
|
663
660
|
}
|
|
664
661
|
function groupAgents(agents) {
|
|
665
662
|
const needs = agents.filter((a) => ['waiting', 'paused'].includes(a.state));
|
package/dist/ui/AttachApp.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Timeline } from './Timeline.js';
|
|
|
12
12
|
import { stateLabel, elapsed, truncate } from './theme.js';
|
|
13
13
|
import { fmtCost } from '../pricing.js';
|
|
14
14
|
import { t } from '../i18n.js';
|
|
15
|
-
import { STATE_META, UI, middleTruncate } from './tokens.js';
|
|
15
|
+
import { COLOR, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
16
16
|
const noop = () => { };
|
|
17
17
|
export function parseAttachCommand(text) {
|
|
18
18
|
const v = text.trim();
|
|
@@ -157,5 +157,5 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
157
157
|
} })) : question ? (_jsx(QuestionPrompt, { question: { ...question, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, answer) => {
|
|
158
158
|
wire({ type: 'answer', id, text: answer });
|
|
159
159
|
setQuestion(null);
|
|
160
|
-
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), context: "attach", targetAgent: info?.name ?? agentRef, modelLabel: info?.model, agentNames: [info?.alias, info?.name, ...others.flatMap((o) => [o.alias, o.name])].filter((n) => Boolean(n)), agents: info ? [info] : [], onSubmit: send, onEscape: () => exit() }), _jsx(Box, {
|
|
160
|
+
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), context: "attach", targetAgent: info?.name ?? agentRef, modelLabel: info?.model, agentNames: [info?.alias, info?.name, ...others.flatMap((o) => [o.alias, o.name])].filter((n) => Boolean(n)), agents: info ? [info] : [], width: process.stdout.columns || 100, onSubmit: send, onEscape: () => exit() }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: COLOR.creamMuted, wrap: "truncate-end", children: formatAttachFooter(info) }) })] }));
|
|
161
161
|
}
|
package/dist/ui/CommandInput.js
CHANGED
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import {
|
|
4
|
+
import { commandPalette } from '../commands.js';
|
|
5
5
|
import { t } from '../i18n.js';
|
|
6
6
|
import { readClipboardImage } from './clipboard.js';
|
|
7
|
+
import { BRAND, COLOR } from './tokens.js';
|
|
7
8
|
/** A paste is "long" when it spans multiple lines — it then collapses into a chip. */
|
|
8
9
|
const PASTE_MIN_LINES = 2;
|
|
9
|
-
const GROUP_LABEL = {
|
|
10
|
-
modes: 'Agent modes',
|
|
11
|
-
control: 'Control',
|
|
12
|
-
views: 'Views',
|
|
13
|
-
settings: 'Settings',
|
|
14
|
-
git: 'Git/session',
|
|
15
|
-
other: 'Other',
|
|
16
|
-
};
|
|
17
10
|
const AGENT_ARG_COMMANDS = new Set(['/focus', '/send', '/attach', '/pause', '/resume', '/stop', '/restore', '/commit']);
|
|
11
|
+
const COMMAND_PAGE_SIZE = 9;
|
|
12
|
+
const PROMPT_GUTTER = '› ';
|
|
18
13
|
function modeHint(value, context, targetAgent) {
|
|
19
14
|
const v = value.trimStart().toLowerCase();
|
|
20
15
|
if (!v) {
|
|
21
16
|
if (context === 'focus')
|
|
22
|
-
return `Message ${targetAgent ?? 'focused agent'} · /
|
|
17
|
+
return `Message ${targetAgent ?? 'focused agent'} · / commands`;
|
|
23
18
|
if (context === 'attach')
|
|
24
|
-
return `Steer ${targetAgent ?? 'agent'} ·
|
|
25
|
-
return '
|
|
19
|
+
return `Steer ${targetAgent ?? 'agent'} · @all broadcasts · /quit detaches`;
|
|
20
|
+
return 'Type a task or / for commands';
|
|
26
21
|
}
|
|
27
22
|
if (!v.startsWith('/')) {
|
|
28
23
|
if (context === 'focus')
|
|
@@ -37,16 +32,10 @@ function modeHint(value, context, targetAgent) {
|
|
|
37
32
|
return 'Task mode · execute, edit, validate';
|
|
38
33
|
if (v.startsWith('/plan') || v === '/p')
|
|
39
34
|
return 'Plan mode · asks before editing';
|
|
40
|
-
return '
|
|
41
|
-
}
|
|
42
|
-
function groupedCommands(commands) {
|
|
43
|
-
const order = ['modes', 'control', 'views', 'settings', 'git', 'other'];
|
|
44
|
-
return order
|
|
45
|
-
.map((g) => [g, commands.filter((c) => (c.group ?? 'other') === g)])
|
|
46
|
-
.filter(([, items]) => items.length > 0);
|
|
35
|
+
return '↑/↓ select · Enter accept';
|
|
47
36
|
}
|
|
48
37
|
export function bestCommandCompletion(value) {
|
|
49
|
-
const cmd =
|
|
38
|
+
const cmd = commandPalette(value)[0];
|
|
50
39
|
return cmd ? `${cmd.name} ` : null;
|
|
51
40
|
}
|
|
52
41
|
export function commandNamesForContext(context) {
|
|
@@ -67,12 +56,36 @@ export function completeAgentArgument(value, agent) {
|
|
|
67
56
|
return value;
|
|
68
57
|
return `${cmd} ${agent} `;
|
|
69
58
|
}
|
|
70
|
-
export function
|
|
59
|
+
export function clampSuggestionIndex(index, count) {
|
|
60
|
+
if (count <= 0)
|
|
61
|
+
return 0;
|
|
62
|
+
return Math.max(0, Math.min(index, count - 1));
|
|
63
|
+
}
|
|
64
|
+
export function wrappedPromptLines(text, width) {
|
|
65
|
+
const usable = Math.max(8, width - PROMPT_GUTTER.length);
|
|
66
|
+
if (!text)
|
|
67
|
+
return [''];
|
|
68
|
+
const lines = [];
|
|
69
|
+
for (const logical of text.split('\n')) {
|
|
70
|
+
if (!logical) {
|
|
71
|
+
lines.push('');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (let i = 0; i < logical.length; i += usable)
|
|
75
|
+
lines.push(logical.slice(i, i + usable));
|
|
76
|
+
}
|
|
77
|
+
return lines.length > 0 ? lines : [''];
|
|
78
|
+
}
|
|
79
|
+
function paintLine(text, width) {
|
|
80
|
+
return text.length >= width ? text.slice(0, width) : text.padEnd(width, ' ');
|
|
81
|
+
}
|
|
82
|
+
export function CommandInput({ active, placeholder, mask, context = 'hub', targetAgent, modelLabel, commandNames, agentNames = [], agents = [], width, onHeightChange, onSubmit, onEscape, notify, }) {
|
|
71
83
|
const [value, setValue] = useState('');
|
|
72
84
|
const [attachments, setAttachments] = useState([]);
|
|
73
85
|
const [history, setHistory] = useState([]);
|
|
74
86
|
const [histIdx, setHistIdx] = useState(-1);
|
|
75
87
|
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
|
88
|
+
const [cursorOn, setCursorOn] = useState(true);
|
|
76
89
|
const attSeq = useRef(0);
|
|
77
90
|
const reset = () => {
|
|
78
91
|
setValue('');
|
|
@@ -118,8 +131,7 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
118
131
|
};
|
|
119
132
|
const uniqueAgentNames = [...new Set(agentNames.filter(Boolean))];
|
|
120
133
|
const allowedCommands = commandNames ?? commandNamesForContext(context);
|
|
121
|
-
const
|
|
122
|
-
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).filter(commandAllowed).slice(0, 10) : [];
|
|
134
|
+
const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? commandPalette(value, { allowedNames: allowedCommands }) : [];
|
|
123
135
|
const agentSuggestions = value.startsWith('@') && !value.includes(' ')
|
|
124
136
|
? ['all', ...uniqueAgentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
|
|
125
137
|
: [];
|
|
@@ -141,23 +153,51 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
141
153
|
useEffect(() => {
|
|
142
154
|
setSelectedSuggestion(0);
|
|
143
155
|
}, [value]);
|
|
156
|
+
const safeSelectedSuggestion = clampSuggestionIndex(selectedSuggestion, suggestionCount);
|
|
144
157
|
useEffect(() => {
|
|
145
|
-
if (selectedSuggestion
|
|
146
|
-
setSelectedSuggestion(
|
|
147
|
-
}, [selectedSuggestion,
|
|
158
|
+
if (selectedSuggestion !== safeSelectedSuggestion)
|
|
159
|
+
setSelectedSuggestion(safeSelectedSuggestion);
|
|
160
|
+
}, [selectedSuggestion, safeSelectedSuggestion]);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (!active)
|
|
163
|
+
return;
|
|
164
|
+
const timer = setInterval(() => setCursorOn((on) => !on), 450);
|
|
165
|
+
return () => clearInterval(timer);
|
|
166
|
+
}, [active]);
|
|
167
|
+
const commandWindowStart = cmdSuggestions.length > COMMAND_PAGE_SIZE
|
|
168
|
+
? Math.min(Math.max(0, safeSelectedSuggestion - Math.floor(COMMAND_PAGE_SIZE / 2)), Math.max(0, cmdSuggestions.length - COMMAND_PAGE_SIZE))
|
|
169
|
+
: 0;
|
|
170
|
+
const shownCommandSuggestions = cmdSuggestions.slice(commandWindowStart, commandWindowStart + COMMAND_PAGE_SIZE);
|
|
171
|
+
const shown = mask ? '•'.repeat(value.length) : value;
|
|
172
|
+
const promptWidth = Math.max(20, width ?? process.stdout.columns ?? 100);
|
|
173
|
+
const inputText = shown || placeholder;
|
|
174
|
+
const promptLines = wrappedPromptLines(inputText, promptWidth);
|
|
175
|
+
const suggestionRows = cmdSuggestions.length > 0
|
|
176
|
+
? shownCommandSuggestions.length + 1
|
|
177
|
+
: agentSuggestions.length > 0
|
|
178
|
+
? agentSuggestions.length
|
|
179
|
+
: argSuggestions.length > 0
|
|
180
|
+
? argSuggestions.length + 1
|
|
181
|
+
: 0;
|
|
182
|
+
const attachmentRows = attachments.length > 0 ? 1 : 0;
|
|
183
|
+
const hintRows = value || context !== 'hub' ? 1 : 0;
|
|
184
|
+
const renderedRows = suggestionRows + attachmentRows + promptLines.length + 2 + hintRows;
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
onHeightChange?.(renderedRows);
|
|
187
|
+
}, [onHeightChange, renderedRows]);
|
|
148
188
|
const completeBest = () => {
|
|
149
189
|
if (cmdSuggestions.length > 0) {
|
|
150
|
-
const cmd = cmdSuggestions[
|
|
190
|
+
const cmd = cmdSuggestions[clampSuggestionIndex(safeSelectedSuggestion, cmdSuggestions.length)];
|
|
151
191
|
setValue(`${cmd.name} `);
|
|
152
192
|
return true;
|
|
153
193
|
}
|
|
154
194
|
if (agentSuggestions.length > 0) {
|
|
155
|
-
const agent = agentSuggestions[
|
|
195
|
+
const agent = agentSuggestions[clampSuggestionIndex(safeSelectedSuggestion, agentSuggestions.length)];
|
|
156
196
|
setValue('@' + agent + ' ');
|
|
157
197
|
return true;
|
|
158
198
|
}
|
|
159
199
|
if (argSuggestions.length > 0) {
|
|
160
|
-
const agent = argSuggestions[
|
|
200
|
+
const agent = argSuggestions[clampSuggestionIndex(safeSelectedSuggestion, argSuggestions.length)];
|
|
161
201
|
setValue(completeAgentArgument(value, agent));
|
|
162
202
|
return true;
|
|
163
203
|
}
|
|
@@ -193,7 +233,7 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
193
233
|
}
|
|
194
234
|
if (key.upArrow) {
|
|
195
235
|
if (hasSuggestions) {
|
|
196
|
-
setSelectedSuggestion((i) => (i - 1
|
|
236
|
+
setSelectedSuggestion((i) => clampSuggestionIndex(i - 1, suggestionCount));
|
|
197
237
|
return;
|
|
198
238
|
}
|
|
199
239
|
setHistIdx((i) => {
|
|
@@ -206,7 +246,7 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
206
246
|
}
|
|
207
247
|
if (key.downArrow) {
|
|
208
248
|
if (hasSuggestions) {
|
|
209
|
-
setSelectedSuggestion((i) => (i + 1
|
|
249
|
+
setSelectedSuggestion((i) => clampSuggestionIndex(i + 1, suggestionCount));
|
|
210
250
|
return;
|
|
211
251
|
}
|
|
212
252
|
setHistIdx((i) => {
|
|
@@ -257,15 +297,21 @@ export function CommandInput({ active, placeholder, mask, context = 'hub', targe
|
|
|
257
297
|
}
|
|
258
298
|
setValue((v) => v + input);
|
|
259
299
|
}, { isActive: active });
|
|
260
|
-
const shown = mask ? '•'.repeat(value.length) : value;
|
|
261
300
|
const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
301
|
+
return (_jsxs(Box, { flexDirection: "column", children: [cmdSuggestions.length > 0 && (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "gray", children: ["Commands ", Math.min(safeSelectedSuggestion + 1, cmdSuggestions.length), "/", cmdSuggestions.length, " \u00B7 \u2191/\u2193"] }), shownCommandSuggestions.map((c, i) => {
|
|
302
|
+
const absolute = commandWindowStart + i;
|
|
303
|
+
const selected = absolute === safeSelectedSuggestion;
|
|
304
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: selected ? COLOR.cream : COLOR.creamMuted, bold: true, children: [selected ? '› ' : ' ', c.name.padEnd(13)] }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name));
|
|
305
|
+
})] })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === safeSelectedSuggestion ? COLOR.cream : COLOR.creamMuted, bold: true, children: [i === safeSelectedSuggestion ? '› ' : ' ', "@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
|
|
267
306
|
? t('input.atAll')
|
|
268
|
-
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })), argSuggestions.length > 0 && (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", bold: true, children: "Agents" }), argSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i ===
|
|
307
|
+
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })), argSuggestions.length > 0 && (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", bold: true, children: "Agents" }), argSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === safeSelectedSuggestion ? COLOR.cream : COLOR.creamMuted, bold: true, children: [i === safeSelectedSuggestion ? '› ' : ' ', n.padEnd(12)] }), _jsx(Text, { color: "gray", children: n === 'all'
|
|
269
308
|
? t('input.atAll')
|
|
270
|
-
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n)))] })), attachments.length > 0 && (_jsx(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: attachments.map((a) => (_jsxs(Text, { color:
|
|
309
|
+
: `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n)))] })), attachments.length > 0 && (_jsx(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: attachments.map((a) => (_jsxs(Text, { color: COLOR.cream, backgroundColor: COLOR.promptBackground, children: [' ', a.kind === 'paste' ? t('input.attPaste', { n: a.n, lines: a.lines }) : t('input.attImage', { n: a.n, file: a.label }), ' '] }, a.n))) })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { backgroundColor: COLOR.promptBackground, children: paintLine('', promptWidth) }), promptLines.map((line, i) => {
|
|
310
|
+
const last = i === promptLines.length - 1;
|
|
311
|
+
const placeholderCursor = !shown && active && cursorOn && i === 0;
|
|
312
|
+
const cursor = shown && active && last && cursorOn ? '█' : '';
|
|
313
|
+
const prefix = i === 0 ? PROMPT_GUTTER : ' ';
|
|
314
|
+
const content = placeholderCursor ? `█${line.slice(1)}` : `${line}${cursor}`;
|
|
315
|
+
return (_jsxs(Text, { backgroundColor: COLOR.promptBackground, children: [_jsx(Text, { color: active ? BRAND.primary : BRAND.muted, backgroundColor: COLOR.promptBackground, bold: true, children: prefix }), _jsx(Text, { color: shown ? 'white' : COLOR.creamMuted, backgroundColor: COLOR.promptBackground, children: paintLine(content, promptWidth - prefix.length) })] }, i));
|
|
316
|
+
}), _jsx(Text, { backgroundColor: COLOR.promptBackground, children: paintLine('', promptWidth) })] }), (value || context !== 'hub') && (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: [modeHint(value, context, targetAgent), targetAgent ? ` · ${targetAgent}` : '', modelLabel && value ? ` · ${modelLabel}` : ''] }))] }));
|
|
271
317
|
}
|
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:
|
|
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:
|
|
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:
|
|
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
|
}) }));
|
package/dist/ui/SettingsPanel.js
CHANGED
|
@@ -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:
|
|
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();
|
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
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
11
|
-
accent:
|
|
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:
|
|
34
|
-
muted:
|
|
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:
|
|
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:
|
|
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
|
|
@@ -79,7 +105,7 @@ export function DiffView({ board, bodyHeight }) {
|
|
|
79
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) => {
|
|
80
106
|
const patch = Diff.createPatch(c.path, c.before, c.after, '', '', { context: 2 });
|
|
81
107
|
const lines = patch.split('\n').slice(4, 34);
|
|
82
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color:
|
|
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));
|
|
83
109
|
}), _jsx(Below, { n: below })] }))] }));
|
|
84
110
|
}
|
|
85
111
|
/** Financial view: live cost / steps / tokens per agent + session total. */
|
|
@@ -90,21 +116,21 @@ export function CostView({ board, bodyHeight }) {
|
|
|
90
116
|
const { slice, above, below } = useScrollWindow(agents, visible, 'top');
|
|
91
117
|
const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
|
|
92
118
|
const unknown = agents.some((a) => a.cost === null);
|
|
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:
|
|
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') })] }));
|
|
94
120
|
}
|
|
95
121
|
/** Skills catalog: user-authored markdown instructions agents can load. */
|
|
96
122
|
export function SkillsView({ skills, bodyHeight }) {
|
|
97
123
|
const fallbackVisible = useVisibleRows(8);
|
|
98
124
|
const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
|
|
99
125
|
const { slice, above, below } = useScrollWindow(skills, visible, 'top');
|
|
100
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor:
|
|
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') })] }));
|
|
101
127
|
}
|
|
102
128
|
/** Specialists catalog: personas (role + optional pinned model). */
|
|
103
129
|
export function SpecialistsView({ specialists, bodyHeight }) {
|
|
104
130
|
const fallbackVisible = useVisibleRows(8);
|
|
105
131
|
const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
|
|
106
132
|
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:
|
|
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') })] }));
|
|
108
134
|
}
|
|
109
135
|
/** Saved sessions: inspect available restore points; restore via /session. */
|
|
110
136
|
export function SessionsView({ projectRoot, bodyHeight }) {
|
|
@@ -118,16 +144,27 @@ export function SessionsView({ projectRoot, bodyHeight }) {
|
|
|
118
144
|
agents: s.data.agents.length,
|
|
119
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') })] }));
|
|
120
146
|
}
|
|
121
|
-
export function HelpView({ bodyHeight }) {
|
|
147
|
+
export function HelpView({ bodyHeight, onSelect }) {
|
|
122
148
|
// Fixed intro/highlight/footer rows consume about 12 lines inside the already-sized body.
|
|
123
149
|
const fallbackVisible = useVisibleRows(16);
|
|
124
150
|
const visible = bodyHeight ? Math.max(3, bodyHeight - 12) : fallbackVisible;
|
|
125
|
-
const commands = visibleCommands();
|
|
126
|
-
const
|
|
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);
|
|
127
161
|
const highlights = [
|
|
128
162
|
['Agent modes', ['/ask', '/task', '/plan']],
|
|
129
163
|
['Shell approvals', ['/approvals ask', '/approvals auto', '/approvals yolo']],
|
|
130
164
|
['Navigation', ['/focus', '/attach', '/raw', '/send']],
|
|
131
165
|
];
|
|
132
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor:
|
|
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') })] }));
|
|
133
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
|
+
}
|
package/dist/version.js
ADDED
package/package.json
CHANGED