@parallel-cli/parallel 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/Md.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { BRAND } from './tokens.js';
3
4
  /**
4
5
  * Mini markdown renderer for agent summaries — line-based, zero deps.
5
6
  * Supports: ## headers, - / * bullets, numbered lists, **bold**, `code`.
@@ -25,15 +26,15 @@ export function Md({ text, dim }) {
25
26
  return _jsx(Text, { children: " " }, i);
26
27
  const header = line.match(/^#{1,3}\s+(.*)$/);
27
28
  if (header) {
28
- return (_jsx(Text, { bold: true, color: "cyanBright", children: header[1] }, i));
29
+ return (_jsx(Text, { bold: true, color: BRAND.primary, children: header[1] }, i));
29
30
  }
30
31
  const bullet = line.match(/^(\s*)[-*]\s+(.*)$/);
31
32
  if (bullet) {
32
- return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [bullet[1], _jsx(Text, { color: "cyan", children: "\u2022 " }), inline(bullet[2], `b${i}`)] }, i));
33
+ return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [bullet[1], _jsx(Text, { color: BRAND.primary, children: "\u2022 " }), inline(bullet[2], `b${i}`)] }, i));
33
34
  }
34
35
  const numbered = line.match(/^(\s*)(\d+)[.)]\s+(.*)$/);
35
36
  if (numbered) {
36
- return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [numbered[1], _jsxs(Text, { color: "cyan", children: [numbered[2], ". "] }), inline(numbered[3], `n${i}`)] }, i));
37
+ return (_jsxs(Text, { wrap: "wrap", dimColor: dim, children: [numbered[1], _jsxs(Text, { color: BRAND.primary, children: [numbered[2], ". "] }), inline(numbered[3], `n${i}`)] }, i));
37
38
  }
38
39
  return (_jsx(Text, { wrap: "wrap", dimColor: dim, children: inline(line, `l${i}`) }, i));
39
40
  }) }));
@@ -6,6 +6,7 @@ import { priceFor } from '../pricing.js';
6
6
  import { SelectList as BaseSelectList } from './Wizard.js';
7
7
  import { LANGS, getLang, setLang, t } from '../i18n.js';
8
8
  import { detectProviderModels, isLocalProvider, isPlaceholderModel, providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
9
+ import { BRAND } from './tokens.js';
9
10
  function masked(key) {
10
11
  if (!key)
11
12
  return '—';
@@ -163,7 +164,7 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
163
164
  setStep(next);
164
165
  };
165
166
  // ---- render ----
166
- return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: scope === 'global' ? t('set.title') : t('sset.title') }), flash ? _jsx(Text, { color: "green", children: flash }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [step.id === 'root' && _jsx(SelectList, { items: rootItems, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
167
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: scope === 'global' ? t('set.title') : t('sset.title') }), flash ? _jsx(Text, { color: "green", children: flash }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [step.id === 'root' && _jsx(SelectList, { items: rootItems, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
167
168
  setLang(code);
168
169
  ctl.setLanguage(code);
169
170
  saved();
@@ -20,6 +20,8 @@ function itemColor(item) {
20
20
  return item.category === 'change' ? UI.warn : UI.muted;
21
21
  if (item.category === 'coordinate')
22
22
  return UI.note;
23
+ if (item.label === 'next')
24
+ return UI.accent;
23
25
  if (item.kind === 'thought')
24
26
  return UI.muted;
25
27
  if (item.kind === 'narration')
@@ -43,6 +45,9 @@ function TimelineRow({ item, cols }) {
43
45
  if (item.kind === 'command') {
44
46
  return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.text, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [t('timeline.ran'), " "] }), _jsx(Text, { color: UI.accent, children: truncate(item.command ?? '', max) })] }), _jsx(OutputLines, { item: item, cols: cols })] }));
45
47
  }
48
+ if (item.label === 'next') {
49
+ return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: UI.accent, wrap: "wrap", children: [_jsx(Text, { color: UI.muted, children: "\u2192 " }), truncate(item.detail ?? '', max)] }) }));
50
+ }
46
51
  if (item.kind === 'files') {
47
52
  const files = item.files ?? [];
48
53
  const shown = files.slice(0, 5).join(', ');
package/dist/ui/Wizard.js CHANGED
@@ -2,6 +2,12 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { t } from '../i18n.js';
5
+ import { BRAND, COLOR } from './tokens.js';
6
+ function clampIndex(index, count) {
7
+ if (count <= 0)
8
+ return 0;
9
+ return Math.max(0, Math.min(index, count - 1));
10
+ }
5
11
  /**
6
12
  * Simple ↑/↓ + Entrée select list. If `allowInput` is set, the user can also
7
13
  * type a free value (e.g. a folder path or a custom model name) — typing
@@ -41,12 +47,12 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
41
47
  }
42
48
  if (key.upArrow) {
43
49
  if (!typing)
44
- setIdx((i) => (i - 1 + selectable.length) % Math.max(1, selectable.length));
50
+ setIdx((i) => clampIndex(i - 1, selectable.length));
45
51
  return;
46
52
  }
47
53
  if (key.downArrow) {
48
54
  if (!typing)
49
- setIdx((i) => (i + 1) % Math.max(1, selectable.length));
55
+ setIdx((i) => clampIndex(i + 1, selectable.length));
50
56
  return;
51
57
  }
52
58
  if (key.pageUp) {
@@ -95,9 +101,9 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, height,
95
101
  const below = Math.max(0, items.length - start - visibleItems.length);
96
102
  return (_jsxs(Box, { flexDirection: "column", children: [above > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", above] }) : null, visibleItems.map((it, localIdx) => {
97
103
  const i = start + localIdx;
98
- return (it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeIdx ? 'cyanBright' : 'gray', bold: !typing && i === safeIdx, children: [!typing && i === safeIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i)));
99
- }), below > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", below] }) : null, allowInput && (_jsx(Box, { marginTop: items.length > 0 ? 1 : 0, children: _jsxs(Text, { color: typing ? 'cyanBright' : 'gray', children: ["\u270E", ' ', typing ? (_jsx(Text, { color: "white", children: mask ? '•'.repeat(typed.length) : typed })) : (_jsx(Text, { color: "gray", children: inputPlaceholder ?? '…' })), typing ? _jsx(Text, { color: "cyanBright", children: "\u2588" }) : null] }) }))] }));
104
+ return (it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeIdx ? COLOR.cream : 'gray', bold: !typing && i === safeIdx, children: [!typing && i === safeIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i)));
105
+ }), below > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", below] }) : null, allowInput && (_jsx(Box, { marginTop: items.length > 0 ? 1 : 0, children: _jsxs(Text, { color: typing ? COLOR.cream : 'gray', children: ["\u270E", ' ', typing ? (_jsx(Text, { color: "white", children: mask ? '•'.repeat(typed.length) : typed })) : (_jsx(Text, { color: "gray", children: inputPlaceholder ?? '…' })), typing ? _jsx(Text, { color: COLOR.cream, children: "\u2588" }) : null] }) }))] }));
100
106
  }
101
107
  export function WizardStep({ step, total, title, children, footer, }) {
102
- return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["[", step, "/", total, "] ", title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: children }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: footer ?? t('wiz.footer.select') }) })] }));
108
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: BRAND.primary, children: ["[", step, "/", total, "] ", title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: children }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: footer ?? t('wiz.footer.select') }) })] }));
103
109
  }
package/dist/ui/events.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { oneLine } from './tokens.js';
2
+ import { t } from '../i18n.js';
2
3
  function cleanToolText(text) {
3
4
  return oneLine(text)
4
- .replace(/^[📖📁🔍✏🚩🧠🧩📢⏳❓✉✅↳]+\s*/u, '')
5
+ .replace(/^[📖📁🔍🔎📚✏🚩🧠🧩📢☑⏳❓✉✅↳]+\s*/u, '')
5
6
  .trim();
6
7
  }
7
8
  function stripShellNoise(command) {
@@ -19,6 +20,12 @@ function classify(log) {
19
20
  if (log.kind === 'tool_result') {
20
21
  return { agentId: log.agentId, kind: 'command_output', label: 'output', detail: log.text.trim(), ts: log.ts, seq: log.seq };
21
22
  }
23
+ if (log.kind === 'tool' && /^\s*📢/u.test(log.text)) {
24
+ return { agentId: log.agentId, kind: 'intent', label: 'next', detail: cleaned || text, ts: log.ts, seq: log.seq };
25
+ }
26
+ if (log.kind === 'tool' && /^\s*☑/u.test(log.text)) {
27
+ return { agentId: log.agentId, kind: 'note', label: 'steps', detail: cleaned || text, ts: log.ts, seq: log.seq };
28
+ }
22
29
  if (log.kind === 'note')
23
30
  return { agentId: log.agentId, kind: 'note', label: 'note', detail: cleaned || text, ts: log.ts, seq: log.seq };
24
31
  if (log.kind === 'system')
@@ -44,8 +51,14 @@ function classify(log) {
44
51
  if (/^(search)\s+/i.test(cleaned)) {
45
52
  return { agentId: log.agentId, kind: 'file', label: 'search', detail: cleaned.replace(/^search\s+/i, ''), ts: log.ts, seq: log.seq };
46
53
  }
47
- if (/^(write|edit|patch|claim|claims?)\s+/i.test(cleaned)) {
48
- const label = lower.startsWith('claim') ? 'claim' : lower.startsWith('write') ? 'write' : 'edit';
54
+ if (/^inspect project/i.test(cleaned)) {
55
+ return { agentId: log.agentId, kind: 'file', label: 'search', detail: 'project', ts: log.ts, seq: log.seq };
56
+ }
57
+ if (/^(claim|claims?):?\s+/i.test(cleaned)) {
58
+ return { agentId: log.agentId, kind: 'note', label: 'claim', detail: cleaned.replace(/^(claim|claims?):?\s*/i, ''), ts: log.ts, seq: log.seq };
59
+ }
60
+ if (/^(write|edit|patch)\s+/i.test(cleaned)) {
61
+ const label = lower.startsWith('write') ? 'write' : 'edit';
49
62
  return { agentId: log.agentId, kind: 'file', label, detail: cleaned.replace(/^(write|edit|patch|claim|claims?)\s*/i, ''), ts: log.ts, seq: log.seq };
50
63
  }
51
64
  if (/^(run|exec|shell|npm|pnpm|yarn|git|node|npx)\b/i.test(cleaned)) {
@@ -105,6 +118,8 @@ function categoryFor(e) {
105
118
  return 'result';
106
119
  if (e.kind === 'note' || e.kind === 'approval' || e.kind === 'question')
107
120
  return 'coordinate';
121
+ if (e.kind === 'intent')
122
+ return 'other';
108
123
  if (e.kind === 'file') {
109
124
  if (e.label === 'write' || e.label === 'edit' || e.label === 'claim')
110
125
  return 'change';
@@ -141,20 +156,8 @@ export function summarizeCommandOutput(output, command = '', maxLines = 6) {
141
156
  }
142
157
  function narrationFor(category, previous) {
143
158
  if (category === 'inspect' && previous === 'validate')
144
- return 'Cette piste ne suffit pas, donc je reviens inspecter le projet pour confirmer l’état réel.';
145
- if (category === 'inspect')
146
- return 'Je vérifie l’état du projet et les fichiers concernés avant de conclure.';
147
- if (category === 'change')
148
- return 'Je modifie maintenant les fichiers ciblés en gardant le changement aussi petit que possible.';
149
- if (category === 'validate')
150
- return 'Je lance les validations locales pour vérifier que les changements tiennent techniquement.';
151
- if (category === 'publish')
152
- return 'Je prépare la synchronisation Git après avoir vérifié l’état local.';
153
- if (category === 'coordinate')
154
- return 'Je traite les échanges et les décisions nécessaires pour avancer proprement.';
155
- if (category === 'result')
156
- return 'Je vérifie le résultat final et les éventuelles erreurs importantes.';
157
- return 'Je poursuis l’activité en cours.';
159
+ return t('timeline.narration.inspectAfterValidate');
160
+ return t(`timeline.narration.${category}`);
158
161
  }
159
162
  function pushSection(out, category, ts, seq) {
160
163
  const prev = [...out].reverse().find((i) => i.kind !== 'section');
package/dist/ui/theme.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { t } from '../i18n.js';
2
- import { STATE } from './tokens.js';
2
+ import { COLOR, STATE } from './tokens.js';
3
3
  /** Strong visual cues: icon + label (i18n key) + color per state. */
4
4
  export const STATE_LABEL = {
5
5
  idle: { icon: '◇', labelKey: 'st.idle', color: STATE.idle },
6
6
  thinking: { icon: '🧠', labelKey: 'st.thinking', color: STATE.thinking },
7
- listening: { icon: '👂', labelKey: 'st.listening', color: 'cyanBright' },
7
+ listening: { icon: '👂', labelKey: 'st.listening', color: COLOR.cream },
8
8
  working: { icon: '🔨', labelKey: 'st.working', color: 'green' },
9
9
  waiting: { icon: '✋', labelKey: 'st.waiting', color: 'magenta' },
10
- paused: { icon: '⏸', labelKey: 'st.paused', color: 'blue' },
10
+ paused: { icon: '⏸', labelKey: 'st.paused', color: COLOR.creamMuted },
11
11
  done: { icon: '✅', labelKey: 'st.done', color: STATE.done },
12
12
  error: { icon: '✖', labelKey: 'st.error', color: 'red' },
13
13
  stopped: { icon: '⏹', labelKey: 'st.stopped', color: STATE.error },
package/dist/ui/tokens.js CHANGED
@@ -6,9 +6,14 @@ export const MARK = {
6
6
  waiting: '?',
7
7
  arrow: '▸',
8
8
  };
9
+ export const COLOR = {
10
+ cream: '#f3e7c7',
11
+ creamMuted: '#c8bfa6',
12
+ promptBackground: '#5f5963',
13
+ };
9
14
  export const UI = {
10
- brand: 'cyanBright',
11
- accent: 'cyan',
15
+ brand: COLOR.cream,
16
+ accent: COLOR.cream,
12
17
  muted: 'gray',
13
18
  text: 'white',
14
19
  ok: 'greenBright',
@@ -30,12 +35,12 @@ export const STATE_META = {
30
35
  // ─── Hub redesign tokens (Phase 0) ───────────────────────────────────────────
31
36
  /** Brand colors — logotype, borders, focus indicator. */
32
37
  export const BRAND = {
33
- primary: 'cyanBright',
34
- muted: 'cyan',
38
+ primary: COLOR.cream,
39
+ muted: COLOR.creamMuted,
35
40
  };
36
41
  /** Semantic state colors mapped to agent states. */
37
42
  export const STATE = {
38
- working: 'cyan',
43
+ working: COLOR.cream,
39
44
  thinking: 'yellow',
40
45
  listening: 'yellow',
41
46
  done: 'greenBright',
@@ -43,11 +48,11 @@ export const STATE = {
43
48
  waiting: 'yellow',
44
49
  idle: 'gray',
45
50
  };
46
- /** Mode indicator colors. `task` is undefined — it renders no mark. */
51
+ /** Mode indicator colors. */
47
52
  export const MODE = {
48
53
  ask: 'yellow',
49
- plan: 'blue',
50
- task: undefined,
54
+ plan: COLOR.creamMuted,
55
+ task: COLOR.cream,
51
56
  };
52
57
  /** Chrome / UI element colors. */
53
58
  export const CHROME = {
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
@@ -47,6 +73,13 @@ function useScrollWindow(items, visible, anchor = 'top') {
47
73
  }
48
74
  const Above = ({ n }) => (n > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", n, " \u00B7 PgUp"] }) : null);
49
75
  const Below = ({ n }) => (n > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", n, " \u00B7 PgDn"] }) : null);
76
+ function shortTime(ts) {
77
+ const seconds = Math.max(0, Math.round((Date.now() - ts) / 1000));
78
+ if (seconds < 60)
79
+ return t('board.secondsAgo', { n: seconds });
80
+ const minutes = Math.round(seconds / 60);
81
+ return t('board.minutesAgo', { n: minutes });
82
+ }
50
83
  /** Usable rows for a view's list, from the REAL terminal height. */
51
84
  function useVisibleRows(overhead, min = 6) {
52
85
  const { stdout } = useStdout();
@@ -55,13 +88,17 @@ function useVisibleRows(overhead, min = 6) {
55
88
  export function BoardView({ board, bodyHeight }) {
56
89
  const agents = [...board.agents.values()];
57
90
  const fallbackVisible = useVisibleRows(12);
58
- const visibleAgents = bodyHeight ? Math.max(1, Math.floor((bodyHeight - 7) / 3)) : fallbackVisible;
91
+ const visibleAgents = bodyHeight ? Math.max(1, Math.floor((bodyHeight - 10) / 3)) : fallbackVisible;
59
92
  const { slice: agentSlice, above, below } = useScrollWindow(agents, visibleAgents, 'top');
60
- const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents - 5) / 2)) : 8;
93
+ const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents - 8) / 2)) : 8;
61
94
  const activities = [...board.fileActivity.values()].sort((a, b) => b.ts - a.ts).slice(0, sideRows);
62
95
  const notes = board.notes.slice(-sideRows);
63
96
  const warnings = board.workMapWarnings.slice(-Math.max(2, Math.min(4, sideRows)));
64
- return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('board.title') }), _jsx(Text, { bold: true, children: t('board.agents') }), agents.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.none')] })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), agentSlice.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name }), _jsxs(Text, { color: STATE_LABEL[a.state].color, children: [' ', STATE_LABEL[a.state].icon, " ", stateLabel(a.state)] }), _jsxs(Text, { color: "gray", children: [" ", truncate(a.currentAction || a.task, 80)] }), a.claims && a.claims.length > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 ", truncate(a.claims.join(', '), 45)] }) : null] }, a.id))), _jsx(Below, { n: below })] })), _jsx(Text, { bold: true, children: t('board.activity') }), warnings.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "yellowBright", children: t('board.workMap') }), warnings.map((w) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: w.level === 'conflict' ? 'redBright' : 'yellow', children: [w.level === 'conflict' ? '!' : '⚠', " "] }), _jsx(Text, { color: "yellow", children: w.title }), _jsxs(Text, { color: "gray", children: [" \u2014 ", truncate(w.detail, 120)] })] }, w.id)))] })) : null, activities.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.noActivity')] })) : (activities.map((act) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', "\u270F ", act.path, " ", _jsxs(Text, { color: "gray", children: ["\u2014 ", act.agentName, " (", act.op, ", ", Math.round((Date.now() - act.ts) / 1000), "s)"] })] }, act.path)))), _jsx(Text, { bold: true, children: t('board.notes') }), notes.map((n) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "magenta", children: [n.from, " \u2192 ", n.to] }), _jsxs(Text, { children: [": ", truncate(n.content, 140)] })] }, n.id)))] }));
97
+ return (_jsxs(Box, { borderStyle: "round", borderColor: BRAND.muted, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: BRAND.primary, children: t('board.title') }), _jsx(Text, { bold: true, color: warnings.length > 0 ? COLOR.cream : BRAND.primary, children: t('board.workMap') }), warnings.length > 0 ? (warnings.map((w) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: w.level === 'conflict' ? 'redBright' : 'yellow', children: [w.level === 'conflict' ? '!' : '⚠', " "] }), _jsx(Text, { color: BRAND.primary, children: w.title }), _jsxs(Text, { color: "gray", children: [" \u2014 ", truncate(w.detail, 100)] })] }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [' ', t('board.warningMeta', {
98
+ agents: w.agentNames.join(', ') || 'agents',
99
+ paths: w.paths.join(', ') || 'paths',
100
+ time: shortTime(w.ts),
101
+ })] })] }, w.id)))) : (_jsxs(Text, { color: "gray", children: [" ", t('board.workMapOk')] })), warnings.length > 0 ? _jsxs(Text, { color: COLOR.creamMuted, children: [" ", t('board.warningSuggestion')] }) : null, _jsx(Text, { bold: true, children: t('board.agents') }), agents.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.none')] })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), agentSlice.map((a) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsx(Text, { color: a.color, bold: true, children: a.name }), _jsxs(Text, { color: STATE_LABEL[a.state].color, children: [' ', STATE_LABEL[a.state].icon, " ", stateLabel(a.state)] }), _jsxs(Text, { color: "gray", children: [" ", truncate(a.currentAction || a.task, 80)] }), a.claims && a.claims.length > 0 ? _jsxs(Text, { color: COLOR.cream, children: [" \u00B7 \u2691 ", truncate(a.claims.join(', '), 45)] }) : null] }, a.id))), _jsx(Below, { n: below })] })), _jsx(Text, { bold: true, children: t('board.activity') }), activities.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", t('board.noActivity')] })) : (activities.map((act) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', "\u270F ", act.path, " ", _jsxs(Text, { color: "gray", children: ["\u2014 ", act.agentName, " (", act.op, ", ", Math.round((Date.now() - act.ts) / 1000), "s)"] })] }, act.path)))), _jsx(Text, { bold: true, children: t('board.notes') }), notes.map((n) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "magenta", children: [n.from, " \u2192 ", n.to] }), _jsxs(Text, { children: [": ", truncate(n.content, 140)] })] }, n.id)))] }));
65
102
  }
66
103
  export function NotesView({ board, bodyHeight }) {
67
104
  const fallbackVisible = useVisibleRows(7);
@@ -76,10 +113,11 @@ export function DiffView({ board, bodyHeight }) {
76
113
  const rows = bodyHeight ? Math.max(8, bodyHeight - 4) : fallbackRows;
77
114
  const perChange = Math.max(1, Math.floor(rows / 34));
78
115
  const { slice: changes, above, below } = useScrollWindow(board.changes, perChange, 'bottom');
79
- 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) => {
116
+ const warnings = board.workMapWarnings.filter((w) => w.level !== 'info').slice(-2);
117
+ 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 }) }), warnings.map((w) => (_jsxs(Text, { color: w.level === 'conflict' ? 'redBright' : 'yellow', wrap: "truncate-end", children: ["\u26A0 ", w.title, ": ", truncate(w.paths.join(', ') || w.detail, 110)] }, w.id))), board.changes.length === 0 ? (_jsx(Text, { color: "gray", children: t('diff.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), changes.map((c) => {
80
118
  const patch = Diff.createPatch(c.path, c.before, c.after, '', '', { context: 2 });
81
119
  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: "cyan", children: c.path }), _jsxs(Text, { color: "gray", children: [' ', t('diff.by', { agent: c.agentName, time: new Date(c.ts).toLocaleTimeString() })] })] }), lines.map((l, i) => (_jsx(Text, { color: l.startsWith('+') ? 'green' : l.startsWith('-') ? 'red' : l.startsWith('@') ? 'cyan' : 'gray', wrap: "truncate-end", children: l || ' ' }, i))), patch.split('\n').length > 38 ? _jsx(Text, { color: "gray", children: t('diff.trunc') }) : null] }, c.id));
120
+ 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
121
  }), _jsx(Below, { n: below })] }))] }));
84
122
  }
85
123
  /** Financial view: live cost / steps / tokens per agent + session total. */
@@ -90,21 +128,21 @@ export function CostView({ board, bodyHeight }) {
90
128
  const { slice, above, below } = useScrollWindow(agents, visible, 'top');
91
129
  const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
92
130
  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: "cyan", children: [String(Math.round(a.tokensIn / 1000)).padStart(5), "k in ", String(Math.round(a.tokensOut / 1000)).padStart(4), "k out", ' '] }), _jsx(Text, { color: "greenBright", bold: true, children: a.cost === null ? ' $—' : fmtCost(a.cost).padStart(8) }), a.cost === null ? _jsxs(Text, { color: "gray", children: [" ", t('cost.unknown')] }) : null] }, a.id))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [' ', t('cost.total'), " ", _jsx(Text, { color: "greenBright", children: fmtCost(total) }), unknown ? _jsxs(Text, { color: "gray", children: [" ", t('cost.partial')] }) : null] })] })), _jsx(Text, { color: "gray", children: t('cost.hint') })] }));
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') })] }));
94
132
  }
95
133
  /** Skills catalog: user-authored markdown instructions agents can load. */
96
134
  export function SkillsView({ skills, bodyHeight }) {
97
135
  const fallbackVisible = useVisibleRows(8);
98
136
  const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
99
137
  const { slice, above, below } = useScrollWindow(skills, visible, 'top');
100
- return (_jsxs(Box, { borderStyle: "round", borderColor: "blueBright", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "blueBright", children: t('skills.title') }), skills.length === 0 ? (_jsx(Text, { color: "gray", children: t('skills.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((s) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "blueBright", bold: true, children: ["#", s.name.padEnd(16)] }), _jsxs(Text, { color: s.scope === 'global' ? 'yellow' : 'green', children: ["[", s.scope, "] "] }), _jsx(Text, { color: "gray", children: truncate(s.description || s.file, 100) })] }, s.file))), _jsx(Below, { n: below })] })), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('skills.hint1') }), _jsx(Text, { color: "gray", children: t('skills.hint2') })] }));
138
+ 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
139
  }
102
140
  /** Specialists catalog: personas (role + optional pinned model). */
103
141
  export function SpecialistsView({ specialists, bodyHeight }) {
104
142
  const fallbackVisible = useVisibleRows(8);
105
143
  const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
106
144
  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: "cyan", children: [s.model, " "] }) : null, _jsx(Text, { color: "gray", children: truncate(s.description || s.file, 90) })] }, s.file))), _jsx(Below, { n: below })] })), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('spec.hint1') }), _jsx(Text, { color: "gray", children: t('spec.hint2') })] }));
145
+ 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
146
  }
109
147
  /** Saved sessions: inspect available restore points; restore via /session. */
110
148
  export function SessionsView({ projectRoot, bodyHeight }) {
@@ -118,16 +156,27 @@ export function SessionsView({ projectRoot, bodyHeight }) {
118
156
  agents: s.data.agents.length,
119
157
  }) }), _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
158
  }
121
- export function HelpView({ bodyHeight }) {
159
+ export function HelpView({ bodyHeight, onSelect }) {
122
160
  // Fixed intro/highlight/footer rows consume about 12 lines inside the already-sized body.
123
161
  const fallbackVisible = useVisibleRows(16);
124
162
  const visible = bodyHeight ? Math.max(3, bodyHeight - 12) : fallbackVisible;
125
- const commands = visibleCommands();
126
- const { slice, above, below } = useScrollWindow(commands, visible, 'top');
163
+ const commands = sortCommandsForPalette(visibleCommands());
164
+ const selected = useSelectableIndex(commands.length, visible);
165
+ useInput((_input, key) => {
166
+ if (key.return)
167
+ onSelect?.(commands[selected]?.name ?? '/help');
168
+ });
169
+ const start = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, commands.length - visible));
170
+ const slice = commands.slice(start, start + visible);
171
+ const above = start;
172
+ const below = Math.max(0, commands.length - start - slice.length);
127
173
  const highlights = [
128
- ['Agent modes', ['/ask', '/task', '/plan']],
174
+ ['Agent modes', ['/ask', '/task', '/plan', '/review']],
129
175
  ['Shell approvals', ['/approvals ask', '/approvals auto', '/approvals yolo']],
130
176
  ['Navigation', ['/focus', '/attach', '/raw', '/send']],
131
177
  ];
132
- return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: t('help.title') }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l1a') }), t('help.l1b'), _jsx(Text, { bold: true, children: t('help.l1c') }), "."] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l2a') }), t('help.l2b'), _jsx(Text, { bold: true, children: t('help.l2c') }), t('help.l2d')] }), _jsx(Text, { wrap: "truncate-end", children: t('help.l3') }), _jsx(Text, { children: " " }), highlights.map(([label, names]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: "cyan", bold: true, children: [label, ": "] }), _jsx(Text, { color: "gray", children: names.join(' ') })] }, label))), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "Keyboard: \u2191/\u2193 or PgUp/PgDn scroll \u00B7 Tab/\u2192 autocomplete \u00B7 Esc back/clear \u00B7 Ctrl+U clear \u00B7 Ctrl+V image" }), _jsx(Text, { children: " " }), _jsx(Above, { n: above }), slice.map((c) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(18) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(24) }), _jsxs(Text, { color: "gray", children: [t(c.descKey), c.aliases?.length ? ` (= ${c.aliases.join(', ')})` : ''] })] }, c.name))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.states') }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.keys') })] }));
178
+ 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) => {
179
+ const isSelected = start + i === selected;
180
+ 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));
181
+ }), _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
182
  }
package/dist/update.js ADDED
@@ -0,0 +1,125 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import readline from 'node:readline';
4
+ import { spawn } from 'node:child_process';
5
+ import { configDir } from './config.js';
6
+ import { PACKAGE_NAME, VERSION } from './version.js';
7
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
8
+ const REMIND_LATER_MS = 24 * 60 * 60 * 1000;
9
+ export function compareVersions(a, b) {
10
+ const pa = a.replace(/^v/, '').split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0);
11
+ const pb = b.replace(/^v/, '').split(/[.-]/).map((n) => Number.parseInt(n, 10) || 0);
12
+ const len = Math.max(pa.length, pb.length);
13
+ for (let i = 0; i < len; i++) {
14
+ const left = pa[i] ?? 0;
15
+ const right = pb[i] ?? 0;
16
+ if (left !== right)
17
+ return left > right ? 1 : -1;
18
+ }
19
+ return 0;
20
+ }
21
+ function updateStateFile() {
22
+ return path.join(configDir(), 'update.json');
23
+ }
24
+ export function readUpdateState() {
25
+ try {
26
+ const file = updateStateFile();
27
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ export function writeUpdateState(state) {
34
+ try {
35
+ fs.mkdirSync(configDir(), { recursive: true });
36
+ fs.writeFileSync(updateStateFile(), JSON.stringify(state, null, 2));
37
+ }
38
+ catch {
39
+ /* best effort */
40
+ }
41
+ }
42
+ export function shouldSkipUpdateCheck(env = process.env) {
43
+ return (env.PARALLEL_SKIP_UPDATE_CHECK === '1' ||
44
+ env.CI === 'true' ||
45
+ env.GITHUB_ACTIONS === 'true' ||
46
+ env.GITLAB_CI === 'true' ||
47
+ env.BUILDKITE === 'true');
48
+ }
49
+ export function shouldPromptForUpdate(info, state, now = Date.now()) {
50
+ if (compareVersions(info.latest, info.current) <= 0)
51
+ return false;
52
+ if (state.dismissedVersion === info.latest)
53
+ return false;
54
+ if (state.skipUntil && state.skipUntil > now)
55
+ return false;
56
+ return true;
57
+ }
58
+ export async function fetchLatestVersion(timeoutMs = 1500) {
59
+ let timeout;
60
+ try {
61
+ const controller = new AbortController();
62
+ timeout = setTimeout(() => controller.abort(), timeoutMs);
63
+ const resp = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`, {
64
+ signal: controller.signal,
65
+ });
66
+ if (!resp.ok)
67
+ return null;
68
+ const data = await resp.json();
69
+ return typeof data.version === 'string' ? data.version : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ finally {
75
+ if (timeout)
76
+ clearTimeout(timeout);
77
+ }
78
+ }
79
+ function ask(question) {
80
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
81
+ return new Promise((resolve) => {
82
+ rl.question(question, (answer) => {
83
+ rl.close();
84
+ resolve(answer.trim());
85
+ });
86
+ });
87
+ }
88
+ export async function runGlobalUpdate() {
89
+ return new Promise((resolve) => {
90
+ const child = spawn('npm', ['install', '-g', PACKAGE_NAME], { stdio: 'inherit' });
91
+ child.on('close', (code) => resolve(code ?? 1));
92
+ child.on('error', () => resolve(1));
93
+ });
94
+ }
95
+ export async function maybeRunStartupUpdate(skip = false) {
96
+ if (skip || shouldSkipUpdateCheck() || !process.stdin.isTTY || !process.stdout.isTTY)
97
+ return false;
98
+ const now = Date.now();
99
+ const state = readUpdateState();
100
+ if (state.lastCheckAt && now - state.lastCheckAt < CHECK_INTERVAL_MS && (!state.skipUntil || state.skipUntil > now)) {
101
+ return false;
102
+ }
103
+ const latest = await fetchLatestVersion();
104
+ writeUpdateState({ ...state, lastCheckAt: now });
105
+ if (!latest)
106
+ return false;
107
+ const info = { current: VERSION, latest };
108
+ if (!shouldPromptForUpdate(info, state, now))
109
+ return false;
110
+ const answer = await ask(`Update Parallel ${VERSION} -> ${latest}? [y/N] `);
111
+ if (!/^y(es)?$/i.test(answer)) {
112
+ writeUpdateState({ ...state, lastCheckAt: now, skipUntil: now + REMIND_LATER_MS });
113
+ return false;
114
+ }
115
+ console.log(`Updating Parallel via \`npm install -g ${PACKAGE_NAME}\`...\n`);
116
+ const code = await runGlobalUpdate();
117
+ if (code === 0) {
118
+ writeUpdateState({ lastCheckAt: now, dismissedVersion: latest });
119
+ console.log('\nUpdate ran successfully! Please restart Parallel.');
120
+ }
121
+ else {
122
+ console.log('\nUpdate failed. Parallel will continue with the current version.');
123
+ }
124
+ return code === 0;
125
+ }
@@ -0,0 +1,2 @@
1
+ export const PACKAGE_NAME = '@parallel-cli/parallel';
2
+ export const VERSION = '0.4.7';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@parallel-cli/parallel",
3
- "version": "0.4.5",
4
- "description": "Real-time multi-agent coding CLI with shared context, adaptive co-editing, dedicated agent terminals, and headless CI runs.",
3
+ "version": "0.4.7",
4
+ "description": "Real-time coding agents that work like a live team on one shared repository.",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "ai",