@parallel-cli/parallel 0.4.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/App.js CHANGED
@@ -644,7 +644,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
644
644
  specialists: 'specialists',
645
645
  };
646
646
  const viewLabel = VIEW_LABEL[view] ?? 'control room';
647
- 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"] }), workMapAlerts.length > 0 ? (_jsxs(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: [" \u00B7 \u26A0 work-map ", workMapAlerts.length] })) : null] }) })) : (_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
647
+ 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"] }), workMapAlerts.length > 0 ? (_jsxs(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: [" \u00B7 \u26A0 work-map ", workMapAlerts.length] })) : null] }) })) : (_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, { ctl: ctl, 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
648
648
  ? systemLines
649
649
  .filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
650
650
  .slice(-2)
@@ -658,7 +658,9 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
658
658
  return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
659
659
  })] })), 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(Text, { children: " " }), _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(Text, { children: " " }), _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 :
660
660
  ctl.session.approvalMode === 'yolo' ? UI.danger :
661
- 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, workMapAlerts.length > 0 ? (_jsx(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: " \u00B7 \u26A0 /board" })) : null, conflictAlerts.length > 0 ? (_jsx(Text, { color: UI.danger, children: " \u00B7 run /review all" })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] }) })] }));
661
+ 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, _jsxs(Text, { color: ctl.projectContextStatus().status === 'ready' ? UI.ok :
662
+ ctl.projectContextStatus().status === 'indexing' ? UI.warn :
663
+ CHROME.muted, children: [" \u00B7 ", t('memory.label'), " ", ctl.projectContextStatus().status] }), _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 index ", ctl.projectIndexStatus().files] }), 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, workMapAlerts.length > 0 ? (_jsx(Text, { color: conflictAlerts.length > 0 ? UI.danger : UI.warn, children: " \u00B7 \u26A0 /board" })) : null, conflictAlerts.length > 0 ? (_jsx(Text, { color: UI.danger, children: " \u00B7 run /review all" })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] }) })] }));
662
664
  }
663
665
  function groupAgents(agents) {
664
666
  const needs = agents.filter((a) => ['waiting', 'paused'].includes(a.state));
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useCallback, useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { createSkillTemplate, createSpecialistTemplate } from '../skills.js';
5
5
  import { priceFor } from '../pricing.js';
@@ -7,6 +7,9 @@ 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
9
  import { BRAND } from './tokens.js';
10
+ function SettingsSelectList({ defaultBack, onBack, ...rest }) {
11
+ return _jsx(BaseSelectList, { ...rest, onBack: onBack ?? defaultBack });
12
+ }
10
13
  function masked(key) {
11
14
  if (!key)
12
15
  return '—';
@@ -40,16 +43,12 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
40
43
  const saved = () => setFlash(t('set.saved'));
41
44
  const cfg = ctl.config;
42
45
  const listHeight = height ? Math.max(3, height - 5) : undefined;
43
- const goBack = () => {
46
+ const goBack = useCallback(() => {
44
47
  if (step.id === 'root')
45
48
  return onClose();
46
49
  setStep(returnStep ?? { id: 'root' });
47
50
  setReturnStep(null);
48
- };
49
- const SelectList = (props) => {
50
- const { onBack, ...rest } = props;
51
- return _jsx(BaseSelectList, { ...rest, onBack: onBack ?? goBack });
52
- };
51
+ }, [onClose, returnStep, step.id]);
53
52
  // ---- root menu items ----
54
53
  const rootItems = scope === 'global'
55
54
  ? [
@@ -164,12 +163,12 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
164
163
  setStep(next);
165
164
  };
166
165
  // ---- render ----
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) => {
166
+ 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(SettingsSelectList, { defaultBack: goBack, items: rootItems, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SettingsSelectList, { defaultBack: goBack, items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
168
167
  setLang(code);
169
168
  ctl.setLanguage(code);
170
169
  saved();
171
170
  setStep({ id: 'root' });
172
- } })), step.id === 'pickProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseProvider') }), _jsx(SelectList, { items: [
171
+ } })), step.id === 'pickProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseProvider') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
173
172
  ...cfg.providers.map((p) => ({ label: p.name, value: p.name, hint: `(${p.baseUrl})` })),
174
173
  { label: t('set.back'), value: '__back__' },
175
174
  ], height: listHeight, onSelect: (v) => {
@@ -183,7 +182,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
183
182
  if (step.next === 'models')
184
183
  return setStep({ id: 'modelList', provider: p });
185
184
  setStep({ id: 'model', provider: p });
186
- } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
185
+ } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
187
186
  ...step.provider.models.map((m) => ({
188
187
  label: m,
189
188
  value: m,
@@ -197,7 +196,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
197
196
  return;
198
197
  }
199
198
  pickModel(step.provider, v);
200
- }, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'endpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.title', { name: step.provider.name }) }), _jsx(Text, { color: "gray", children: step.provider.baseUrl }), _jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.model', { model: step.provider.defaultModel || step.provider.models[0] || '—' }) }), _jsx(SelectList, { items: [
199
+ }, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'endpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.title', { name: step.provider.name }) }), _jsx(Text, { color: "gray", children: step.provider.baseUrl }), _jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.model', { model: step.provider.defaultModel || step.provider.models[0] || '—' }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
201
200
  { label: t('wiz.provider.endpoint.use'), value: 'use' },
202
201
  { label: t('wiz.provider.endpoint.edit'), value: 'edit' },
203
202
  { label: t('set.back'), value: '__back__' },
@@ -220,7 +219,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
220
219
  saved();
221
220
  setStep(returnStep ?? { id: 'providerDetail', provider: step.provider, scope });
222
221
  setReturnStep(null);
223
- } })] })), step.id === 'setupScope' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.setupScope.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
222
+ } })] })), step.id === 'setupScope' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.setupScope.title', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
224
223
  { label: t('set.setupScope.session'), value: 'session' },
225
224
  { label: t('set.setupScope.global'), value: 'global' },
226
225
  { label: t('set.back'), value: '__back__' },
@@ -228,10 +227,10 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
228
227
  if (v === '__back__')
229
228
  return setStep({ id: 'endpoint', provider: step.provider, setup: true });
230
229
  finishProviderSetup(step.provider, v === 'global');
231
- } })] })), step.id === 'editEndpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: step.provider.baseUrl, onInput: (url) => {
230
+ } })] })), step.id === 'editEndpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: step.provider.baseUrl, onInput: (url) => {
232
231
  const provider = { ...step.provider, baseUrl: url.trim() };
233
232
  setStep({ id: 'endpoint', provider, setup: step.setup });
234
- } })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
233
+ } })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
235
234
  ...step.provider.models.map((m) => ({
236
235
  label: m,
237
236
  value: m,
@@ -262,7 +261,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
262
261
  saved();
263
262
  setStep(returnStep ?? { id: 'root' });
264
263
  setReturnStep(null);
265
- } })] })), step.id === 'priceModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
264
+ } })] })), step.id === 'priceModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceModel', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
266
265
  ...step.provider.models.map((m) => {
267
266
  const pr = priceFor(step.provider, m);
268
267
  return {
@@ -281,7 +280,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
281
280
  return;
282
281
  }
283
282
  goSub({ id: 'priceValue', provider: step.provider, model: v });
284
- }, onInput: (m) => goSub({ id: 'priceValue', provider: step.provider, model: m.trim() }) })] })), step.id === 'priceValue' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceValue', { model: step.model }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
283
+ }, onInput: (m) => goSub({ id: 'priceValue', provider: step.provider, model: m.trim() }) })] })), step.id === 'priceValue' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceValue', { model: step.model }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
285
284
  const m = v.match(/^\s*([\d.]+)\s*[,;\s]\s*([\d.]+)\s*$/);
286
285
  if (!m)
287
286
  return setFlash(t('set.priceBad'));
@@ -293,7 +292,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
293
292
  saved();
294
293
  setStep(returnStep ?? { id: 'root' });
295
294
  setReturnStep(null);
296
- } })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
295
+ } })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
297
296
  const provider = { ...step.provider, apiKey: k.trim() };
298
297
  if (step.setup) {
299
298
  if (scope === 'session') {
@@ -309,7 +308,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
309
308
  }
310
309
  setStep(returnStep ?? { id: 'root' });
311
310
  setReturnStep(null);
312
- } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onInput: (name) => setStep({ id: 'newUrl', name }) })] })), step.id === 'newUrl' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onInput: (url) => setStep({ id: 'newModel', name: step.name, url }) })] })), step.id === 'newModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.model.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => {
311
+ } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onInput: (name) => setStep({ id: 'newUrl', name }) })] })), step.id === 'newUrl' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onInput: (url) => setStep({ id: 'newModel', name: step.name, url }) })] })), step.id === 'newModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.model.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => {
313
312
  const trimmed = model.trim();
314
313
  if (isPlaceholderModel(trimmed)) {
315
314
  setFlash(t('set.modelPlaceholder'));
@@ -328,7 +327,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
328
327
  requiresApiKey: !local,
329
328
  },
330
329
  });
331
- } })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishProviderSetup({ name: step.name, baseUrl: step.url, apiKey: key.trim(), models: [step.model], defaultModel: step.model }) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
330
+ } })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishProviderSetup({ name: step.name, baseUrl: step.url, apiKey: key.trim(), models: [step.model], defaultModel: step.model }) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
332
331
  try {
333
332
  const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
334
333
  setFlash(t('m.skillCreated', { file }));
@@ -337,7 +336,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
337
336
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
338
337
  }
339
338
  setStep({ id: 'root' });
340
- } })] })), step.id === 'newSpecialist' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSpecialistName') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "reviewer, architect, tester\u2026", onInput: (name) => {
339
+ } })] })), step.id === 'newSpecialist' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSpecialistName') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [], height: listHeight, allowInput: true, inputPlaceholder: "reviewer, architect, tester\u2026", onInput: (name) => {
341
340
  try {
342
341
  const file = createSpecialistTemplate(name.trim(), '', 'global', ctl.projectRoot);
343
342
  setFlash(t('m.specCreated', { file }));
@@ -346,7 +345,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
346
345
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
347
346
  }
348
347
  setStep({ id: 'root' });
349
- } })] })), step.id === 'providers' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: step.scope === 'global' ? t('set.providers.title') : t('sset.providers.title') }), _jsx(SelectList, { items: (() => {
348
+ } })] })), step.id === 'providers' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: step.scope === 'global' ? t('set.providers.title') : t('sset.providers.title') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: (() => {
350
349
  const configuredNames = new Set(cfg.providers.map((p) => p.name.toLowerCase()));
351
350
  const items = [];
352
351
  // Section: Configured
@@ -443,7 +442,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
443
442
  else {
444
443
  setStep({ id: 'providerDetail', provider: p, scope: 'global' });
445
444
  }
446
- } })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
445
+ } })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
447
446
  {
448
447
  label: t('set.providerDetail.key'),
449
448
  value: 'key',
@@ -508,7 +507,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
508
507
  }
509
508
  if (v === 'remove')
510
509
  return setStep({ id: 'removeProvider', provider: step.provider, scope: step.scope });
511
- } })] })), step.id === 'removeProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.removeProvider.title', { name: step.provider.name }) }), _jsx(Text, { color: "yellow", children: t('set.removeProvider.confirm') }), _jsx(SelectList, { items: [
510
+ } })] })), step.id === 'removeProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.removeProvider.title', { name: step.provider.name }) }), _jsx(Text, { color: "yellow", children: t('set.removeProvider.confirm') }), _jsx(SettingsSelectList, { defaultBack: goBack, items: [
512
511
  { label: t('set.removeProvider.yes'), value: 'yes' },
513
512
  { label: t('set.removeProvider.no'), value: 'no' },
514
513
  ], height: listHeight, onSelect: (v) => {
package/dist/ui/Wizard.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useEffect, useMemo, useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { t } from '../i18n.js';
5
5
  import { BRAND, COLOR } from './tokens.js';
@@ -8,6 +8,16 @@ function clampIndex(index, count) {
8
8
  return 0;
9
9
  return Math.max(0, Math.min(index, count - 1));
10
10
  }
11
+ export function selectableIndexes(items) {
12
+ return items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
13
+ }
14
+ export function selectListWindow(itemsLength, selectedRealIndex, maxVisible) {
15
+ const visible = Math.max(1, maxVisible);
16
+ if (itemsLength <= visible || selectedRealIndex < 0)
17
+ return { start: 0, end: Math.min(itemsLength, visible) };
18
+ const start = Math.max(0, Math.min(selectedRealIndex - Math.floor(visible / 2), itemsLength - visible));
19
+ return { start, end: start + visible };
20
+ }
11
21
  /**
12
22
  * Simple ↑/↓ + Entrée select list. If `allowInput` is set, the user can also
13
23
  * type a free value (e.g. a folder path or a custom model name) — typing
@@ -17,9 +27,25 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
17
27
  const [idx, setIdx] = useState(0);
18
28
  const [typed, setTyped] = useState('');
19
29
  const typing = allowInput && typed.length > 0;
30
+ const selectable = useMemo(() => selectableIndexes(items), [items]);
31
+ const safeLogicalIdx = clampIndex(idx, selectable.length);
32
+ const safeRealIdx = selectable.length > 0 ? selectable[safeLogicalIdx] : -1;
33
+ const maxVisible = height ? Math.max(1, height - (allowInput ? 2 : 0)) : items.length;
34
+ const window = selectListWindow(items.length, safeRealIdx, maxVisible);
35
+ const visibleItems = items.slice(window.start, window.end);
36
+ const above = window.start;
37
+ const below = Math.max(0, items.length - window.end);
38
+ const pageStep = Math.max(1, Math.floor(Math.max(1, maxVisible) / 2));
39
+ useEffect(() => {
40
+ if (safeLogicalIdx !== idx)
41
+ setIdx(safeLogicalIdx);
42
+ }, [idx, safeLogicalIdx]);
43
+ const chooseCurrent = () => {
44
+ const realIdx = selectable[safeLogicalIdx];
45
+ if (realIdx !== undefined && items[realIdx])
46
+ onSelect?.(items[realIdx].value);
47
+ };
20
48
  useInput((input, key) => {
21
- // Build selectable index list each render (cheap — items is small).
22
- const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
23
49
  if (key.escape) {
24
50
  if (typed)
25
51
  setTyped('');
@@ -27,6 +53,13 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
27
53
  onBack?.();
28
54
  return;
29
55
  }
56
+ if (key.leftArrow) {
57
+ if (typed)
58
+ setTyped('');
59
+ else
60
+ onBack?.();
61
+ return;
62
+ }
30
63
  if (key.return) {
31
64
  if (typing) {
32
65
  const v = typed.trim();
@@ -35,14 +68,19 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
35
68
  onInput?.(v);
36
69
  }
37
70
  else {
38
- const realIdx = selectable[idx];
39
- if (realIdx !== undefined && items[realIdx])
40
- onSelect?.(items[realIdx].value);
71
+ chooseCurrent();
41
72
  }
42
73
  return;
43
74
  }
75
+ if ((key.tab || key.rightArrow) && !typing) {
76
+ chooseCurrent();
77
+ return;
78
+ }
44
79
  if (key.backspace || key.delete) {
45
- setTyped((v) => v.slice(0, -1));
80
+ if (typed)
81
+ setTyped((v) => v.slice(0, -1));
82
+ else
83
+ onBack?.();
46
84
  return;
47
85
  }
48
86
  if (key.upArrow) {
@@ -57,12 +95,12 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
57
95
  }
58
96
  if (key.pageUp) {
59
97
  if (!typing)
60
- setIdx((i) => Math.max(0, i - Math.max(1, Math.floor((height ?? 8) / 2))));
98
+ setIdx((i) => clampIndex(i - pageStep, selectable.length));
61
99
  return;
62
100
  }
63
101
  if (key.pageDown) {
64
102
  if (!typing)
65
- setIdx((i) => Math.min(Math.max(0, selectable.length - 1), i + Math.max(1, Math.floor((height ?? 8) / 2))));
103
+ setIdx((i) => clampIndex(i + pageStep, selectable.length));
66
104
  return;
67
105
  }
68
106
  if (key.home) {
@@ -89,19 +127,9 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
89
127
  }
90
128
  setTyped((v) => v + input);
91
129
  });
92
- // Build a separate index map so up/down skip section headers.
93
- const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
94
- const safeIdx = selectable.length > 0 ? selectable[Math.min(idx, selectable.length - 1)] : -1;
95
- const maxVisible = height ? Math.max(1, height - (allowInput ? 2 : 0)) : items.length;
96
- const start = items.length > maxVisible && safeIdx >= 0
97
- ? Math.max(0, Math.min(safeIdx - Math.floor(maxVisible / 2), items.length - maxVisible))
98
- : 0;
99
- const visibleItems = items.slice(start, start + maxVisible);
100
- const above = start;
101
- const below = Math.max(0, items.length - start - visibleItems.length);
102
130
  return (_jsxs(Box, { flexDirection: "column", children: [above > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", above] }) : null, visibleItems.map((it, localIdx) => {
103
- const i = start + localIdx;
104
- return (it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeIdx ? COLOR.cream : 'gray', bold: !typing && i === safeIdx, children: [!typing && i === safeIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i)));
131
+ const i = window.start + localIdx;
132
+ return (it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeRealIdx ? COLOR.cream : 'gray', bold: !typing && i === safeRealIdx, children: [!typing && i === safeRealIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i)));
105
133
  }), below > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", below] }) : null, allowInput && (_jsx(Box, { marginTop: items.length > 0 ? 1 : 0, children: _jsxs(Text, { color: typing ? COLOR.cream : 'gray', children: ["\u270E", ' ', typing ? (_jsx(Text, { color: "white", children: mask ? '•'.repeat(typed.length) : typed })) : (_jsx(Text, { color: "gray", children: inputPlaceholder ?? '…' })), typing ? _jsx(Text, { color: COLOR.cream, children: "\u2588" }) : null] }) }))] }));
106
134
  }
107
135
  export function WizardStep({ step, total, title, children, footer, }) {
package/dist/ui/views.js CHANGED
@@ -121,14 +121,16 @@ export function DiffView({ board, bodyHeight }) {
121
121
  }), _jsx(Below, { n: below })] }))] }));
122
122
  }
123
123
  /** Financial view: live cost / steps / tokens per agent + session total. */
124
- export function CostView({ board, bodyHeight }) {
124
+ export function CostView({ ctl, bodyHeight }) {
125
+ const board = ctl.board;
125
126
  const agents = [...board.agents.values()];
126
127
  const fallbackVisible = useVisibleRows(8);
127
128
  const visible = bodyHeight ? Math.max(3, bodyHeight - 7) : fallbackVisible;
128
129
  const { slice, above, below } = useScrollWindow(agents, visible, 'top');
129
130
  const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
131
+ const memory = ctl.projectContextStatus();
130
132
  const unknown = agents.some((a) => a.cost === null);
131
- return (_jsxs(Box, { borderStyle: "round", borderColor: "greenBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "greenBright", children: t('cost.title') }), agents.length === 0 ? (_jsx(Text, { color: "gray", children: t('cost.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name.padEnd(12) }), _jsxs(Text, { color: "gray", children: [a.model.padEnd(24).slice(0, 24), " "] }), _jsxs(Text, { children: [String(a.steps).padStart(3), " steps "] }), _jsxs(Text, { color: BRAND.primary, children: [String(Math.round(a.tokensIn / 1000)).padStart(5), "k in ", String(Math.round(a.tokensOut / 1000)).padStart(4), "k out", ' '] }), _jsx(Text, { color: "greenBright", bold: true, children: a.cost === null ? ' $—' : fmtCost(a.cost).padStart(8) }), a.cost === null ? _jsxs(Text, { color: "gray", children: [" ", t('cost.unknown')] }) : null] }, a.id))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [' ', t('cost.total'), " ", _jsx(Text, { color: "greenBright", children: fmtCost(total) }), unknown ? _jsxs(Text, { color: "gray", children: [" ", t('cost.partial')] }) : null] })] })), _jsx(Text, { color: "gray", children: t('cost.hint') })] }));
133
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "greenBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "greenBright", children: t('cost.title') }), agents.length === 0 ? (_jsx(Text, { color: "gray", children: t('cost.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name.padEnd(12) }), _jsxs(Text, { color: "gray", children: [a.model.padEnd(24).slice(0, 24), " "] }), _jsxs(Text, { color: "gray", children: [(a.profile ?? 'standard').padEnd(8), " "] }), _jsxs(Text, { children: [String(a.steps).padStart(3), " steps "] }), _jsxs(Text, { color: BRAND.primary, children: [String(Math.round(a.tokensIn / 1000)).padStart(5), "k in ", String(Math.round(a.tokensOut / 1000)).padStart(4), "k out", ' '] }), _jsx(Text, { color: "greenBright", bold: true, children: a.cost === null ? ' $—' : fmtCost(a.cost).padStart(8) }), a.cost === null ? _jsxs(Text, { color: "gray", children: [" ", t('cost.unknown')] }) : null] }, a.id))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [' ', t('cost.total'), " ", _jsx(Text, { color: "greenBright", children: fmtCost(total) }), unknown ? _jsxs(Text, { color: "gray", children: [" ", t('cost.partial')] }) : null] })] })), _jsxs(Text, { children: [' ', t('cost.memory'), " ", _jsx(Text, { color: BRAND.primary, children: memory.model ?? '—' }), ' ', _jsx(Text, { color: "greenBright", children: memory.cost === null ? '$—' : fmtCost(memory.cost) }), ' ', _jsxs(Text, { color: "gray", children: ["(", memory.status, ", ", memory.tokensIn + memory.tokensOut, " tokens)"] })] }), _jsx(Text, { color: "gray", children: t('cost.hint') })] }));
132
134
  }
133
135
  /** Skills catalog: user-authored markdown instructions agents can load. */
134
136
  export function SkillsView({ skills, bodyHeight }) {
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const PACKAGE_NAME = '@parallel-cli/parallel';
2
- export const VERSION = '0.4.7';
2
+ export const VERSION = '0.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallel-cli/parallel",
3
- "version": "0.4.9",
3
+ "version": "0.5.0",
4
4
  "description": "Real-time coding agents that work like a live team on one shared repository.",
5
5
  "keywords": [
6
6
  "cli",
@@ -48,7 +48,7 @@
48
48
  "scripts": {
49
49
  "build": "tsc && chmod +x dist/index.js",
50
50
  "dev": "tsc --watch",
51
- "test": "npm run build && ( [ -d test ] && node --test test/*.test.mjs && npm run test:pty || echo 'test/ directory not found, skipping tests' )",
51
+ "test": "npm run build && if [ -d test ]; then node --test test/*.test.mjs && npm run test:pty; else echo 'test/ directory not found, skipping tests'; fi",
52
52
  "test:pty": "sh test/pty.sh",
53
53
  "start": "node dist/index.js",
54
54
  "prepublishOnly": "npm run build"