@parallel-cli/parallel 0.3.3 → 0.4.1

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.
@@ -6,7 +6,39 @@ import { t } from '../i18n.js';
6
6
  import { readClipboardImage } from './clipboard.js';
7
7
  /** A paste is "long" when it spans multiple lines — it then collapses into a chip. */
8
8
  const PASTE_MIN_LINES = 2;
9
- export function CommandInput({ active, placeholder, mask, agentNames = [], onSubmit, onEscape, notify }) {
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
+ function modeHint(value) {
18
+ const v = value.trimStart().toLowerCase();
19
+ if (!v)
20
+ return 'Default /task · Tab/→ autocomplete · / for commands';
21
+ if (!v.startsWith('/'))
22
+ return 'Will launch /task';
23
+ if (v.startsWith('/ask') || v === '/a')
24
+ return 'Ask mode · advice only · no edits';
25
+ if (v.startsWith('/task') || v === '/t')
26
+ return 'Task mode · execute, edit, validate';
27
+ if (v.startsWith('/plan') || v === '/p')
28
+ return 'Plan mode · asks before editing';
29
+ return 'Tab/→ accepts the best suggestion';
30
+ }
31
+ function groupedCommands(commands) {
32
+ const order = ['modes', 'control', 'views', 'settings', 'git', 'other'];
33
+ return order
34
+ .map((g) => [g, commands.filter((c) => (c.group ?? 'other') === g)])
35
+ .filter(([, items]) => items.length > 0);
36
+ }
37
+ export function bestCommandCompletion(value) {
38
+ const cmd = matchCommands(value)[0];
39
+ return cmd ? `${cmd.name} ` : null;
40
+ }
41
+ export function CommandInput({ active, placeholder, mask, agentNames = [], agents = [], onSubmit, onEscape, notify }) {
10
42
  const [value, setValue] = useState('');
11
43
  const [attachments, setAttachments] = useState([]);
12
44
  const [history, setHistory] = useState([]);
@@ -53,6 +85,22 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], onSub
53
85
  setAttachments((arr) => [...arr, { kind: 'image', n, dataUri: img.dataUri, label: img.label }]);
54
86
  notify?.(t('input.imageAdded'));
55
87
  };
88
+ const completeBest = () => {
89
+ const cmd = bestCommandCompletion(value);
90
+ if (cmd) {
91
+ setValue(cmd);
92
+ return true;
93
+ }
94
+ if (value.startsWith('@')) {
95
+ const frag = value.slice(1).toLowerCase();
96
+ const m = ['all', ...agentNames].find((n) => n.toLowerCase().startsWith(frag));
97
+ if (m) {
98
+ setValue('@' + m + ' ');
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ };
56
104
  useInput((input, key) => {
57
105
  if (key.escape) {
58
106
  if (value || attachments.length > 0)
@@ -100,18 +148,8 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], onSub
100
148
  });
101
149
  return;
102
150
  }
103
- if (key.tab) {
104
- const cmds = matchCommands(value);
105
- if (cmds.length > 0) {
106
- setValue(cmds[0].name + ' ');
107
- return;
108
- }
109
- if (value.startsWith('@')) {
110
- const frag = value.slice(1).toLowerCase();
111
- const m = ['all', ...agentNames].find((n) => n.toLowerCase().startsWith(frag));
112
- if (m)
113
- setValue('@' + m + ' ');
114
- }
151
+ if (key.tab || key.rightArrow) {
152
+ completeBest();
115
153
  return;
116
154
  }
117
155
  if (key.ctrl && input === 'u') {
@@ -145,10 +183,13 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], onSub
145
183
  }
146
184
  setValue((v) => v + input);
147
185
  }, { isActive: active });
148
- const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0, 8) : [];
186
+ const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0, 10) : [];
149
187
  const agentSuggestions = value.startsWith('@') && !value.includes(' ')
150
188
  ? ['all', ...agentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
151
189
  : [];
152
190
  const shown = mask ? '•'.repeat(value.length) : value;
153
- return (_jsxs(Box, { flexDirection: "column", children: [cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: cmdSuggestions.map((c) => (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(18) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(24) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n) => (_jsxs(Text, { children: [_jsxs(Text, { color: "magenta", bold: true, children: ["@", n] }), _jsxs(Text, { color: "gray", children: [t('input.atHint'), n === 'all' ? t('input.atAll') : ''] })] }, n))) })), attachments.length > 0 && (_jsx(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: attachments.map((a) => (_jsxs(Text, { color: "cyan", backgroundColor: "gray", 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, { borderStyle: "round", borderColor: active ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u276F", ' '] }), shown ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: shown }), active && _jsx(Text, { color: "cyanBright", children: "\u2588" })] })) : (_jsxs(_Fragment, { children: [active && _jsx(Text, { color: "cyanBright", children: "\u2588" }), _jsx(Text, { color: "gray", children: placeholder })] }))] })] }));
191
+ const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
192
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value) }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(14) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(22) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name)))] }, group))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", bold: true, children: ["@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
193
+ ? t('input.atAll')
194
+ : `${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: "cyan", backgroundColor: "gray", 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, { borderStyle: "single", borderColor: active ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u203A", ' '] }), shown ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: shown }), active && _jsx(Text, { color: "cyanBright", children: "\u2588" })] })) : (_jsxs(_Fragment, { children: [active && _jsx(Text, { color: "cyanBright", children: "\u2588" }), _jsx(Text, { color: "gray", children: placeholder })] }))] })] }));
154
195
  }
@@ -10,75 +10,82 @@ function masked(key) {
10
10
  return '—';
11
11
  return '••••' + key.slice(-4);
12
12
  }
13
+ function nextApprovalMode(mode) {
14
+ if (mode === 'ask')
15
+ return 'auto-safe';
16
+ if (mode === 'auto-safe')
17
+ return 'yolo';
18
+ return 'ask';
19
+ }
20
+ /** Derive a status badge string for a provider in the submenu list. */
21
+ function providerStatus(p, defaultName) {
22
+ const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(p.baseUrl);
23
+ if (p.name.toLowerCase() === defaultName.toLowerCase())
24
+ return t('set.status.default');
25
+ if (isLocal)
26
+ return t('set.status.local');
27
+ if (p.apiKey)
28
+ return masked(p.apiKey);
29
+ return t('set.status.noKey');
30
+ }
13
31
  /**
14
32
  * /settings → scope 'global' : persisted in ~/.parallel/config.json
15
33
  * /settings-session → scope 'session' : this session only, never persisted
16
34
  */
17
35
  export function SettingsPanel({ ctl, scope, onClose, }) {
18
36
  const [step, setStep] = useState({ id: 'root' });
37
+ const [returnStep, setReturnStep] = useState(null);
19
38
  const [flash, setFlash] = useState('');
20
39
  const saved = () => setFlash(t('set.saved'));
21
40
  const cfg = ctl.config;
41
+ // ---- root menu items ----
22
42
  const rootItems = scope === 'global'
23
43
  ? [
24
44
  { label: t('set.language', { lang: LANGS.find((l) => l.code === getLang())?.label ?? getLang() }), value: 'lang' },
25
45
  {
26
46
  label: t('set.defaultPM', {
27
- pm: cfg.defaultProvider ? `${cfg.defaultProvider}:${cfg.providers.find((p) => p.name === cfg.defaultProvider)?.defaultModel ?? '?'}` : '—',
47
+ pm: cfg.defaultProvider
48
+ ? `${cfg.defaultProvider}:${cfg.providers.find((p) => p.name === cfg.defaultProvider)?.defaultModel ?? '?'}`
49
+ : '—',
28
50
  }),
29
51
  value: 'defaultPM',
30
52
  },
31
- ...cfg.providers.map((p) => ({
32
- label: t('set.key', { name: p.name, masked: masked(p.apiKey) }),
33
- value: `key:${p.name}`,
34
- })),
35
- { label: t('set.addProvider'), value: 'add' },
36
- { label: t('set.models'), value: 'models' },
37
- { label: t('set.prices'), value: 'prices' },
38
- { label: t('set.newSkill'), value: 'newSkill' },
39
- { label: t('set.newSpecialist'), value: 'newSpecialist' },
53
+ { label: t('set.providers'), value: 'providers' },
40
54
  { label: t('set.approvals', { mode: cfg.approvalMode }), value: 'approvals' },
41
55
  { label: t('set.sound', { state: cfg.soundEnabled ? 'on' : 'off' }), value: 'sound' },
56
+ { label: t('set.newSkill'), value: 'newSkill' },
57
+ { label: t('set.newSpecialist'), value: 'newSpecialist' },
42
58
  { label: t('set.back'), value: 'back' },
43
59
  ]
44
60
  : [
45
61
  {
46
62
  label: t('sset.model', { pm: `${ctl.session.providerName || '—'}:${ctl.session.model || '—'}` }),
47
- value: 'model',
63
+ value: 'providers',
48
64
  },
49
65
  { label: t('sset.approvals', { mode: ctl.session.approvalMode }), value: 'approvals' },
50
66
  { label: t('sset.sound', { state: ctl.session.soundEnabled ? 'on' : 'off' }), value: 'sound' },
51
67
  { label: t('set.back'), value: 'back' },
52
68
  ];
69
+ // ---- root menu handler ----
53
70
  const chooseRoot = (v) => {
54
71
  setFlash('');
55
72
  if (v === 'back')
56
73
  return onClose();
57
74
  if (v === 'lang')
58
75
  return setStep({ id: 'lang' });
59
- if (v === 'defaultPM' || v === 'model')
76
+ if (v === 'defaultPM')
60
77
  return setStep({ id: 'pickProvider', next: 'model' });
61
- if (v.startsWith('key:')) {
62
- const p = cfg.providers.find((x) => x.name === v.slice(4));
63
- if (p)
64
- setStep({ id: 'key', provider: p });
65
- return;
66
- }
67
- if (v === 'add')
68
- return setStep({ id: 'newName' });
69
- if (v === 'models')
70
- return setStep({ id: 'pickProvider', next: 'models' });
71
- if (v === 'prices')
72
- return setStep({ id: 'pickProvider', next: 'prices' });
78
+ if (v === 'providers')
79
+ return setStep({ id: 'providers', scope });
73
80
  if (v === 'newSkill')
74
81
  return setStep({ id: 'newSkill' });
75
82
  if (v === 'newSpecialist')
76
83
  return setStep({ id: 'newSpecialist' });
77
84
  if (v === 'approvals') {
78
85
  if (scope === 'global')
79
- ctl.setGlobalApprovalMode(cfg.approvalMode === 'ask' ? 'auto' : 'ask');
86
+ ctl.setGlobalApprovalMode(nextApprovalMode(cfg.approvalMode));
80
87
  else
81
- ctl.setSessionApprovalMode(ctl.session.approvalMode === 'ask' ? 'auto' : 'ask');
88
+ ctl.setSessionApprovalMode(nextApprovalMode(ctl.session.approvalMode));
82
89
  if (scope === 'global')
83
90
  saved();
84
91
  return;
@@ -93,6 +100,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
93
100
  return;
94
101
  }
95
102
  };
103
+ // ---- shared helpers ----
96
104
  const pickModel = (provider, model) => {
97
105
  if (scope === 'global') {
98
106
  provider.defaultModel = model;
@@ -105,13 +113,21 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
105
113
  else {
106
114
  ctl.setSessionModel(`${provider.name}:${model}`);
107
115
  }
108
- setStep({ id: 'root' });
116
+ setStep(returnStep ?? { id: 'root' });
117
+ setReturnStep(null);
109
118
  };
110
119
  const finishNewProvider = (name, url, model, key) => {
111
120
  ctl.saveProvider({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
112
121
  saved();
113
- setStep({ id: 'root' });
122
+ setStep(returnStep ?? { id: 'root' });
123
+ setReturnStep(null);
114
124
  };
125
+ // ---- navigate into a sub-step, remembering where to return ----
126
+ const goSub = (next) => {
127
+ setReturnStep(step);
128
+ setStep(next);
129
+ };
130
+ // ---- render ----
115
131
  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, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), onSelect: (code) => {
116
132
  setLang(code);
117
133
  ctl.setLanguage(code);
@@ -131,7 +147,21 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
131
147
  if (step.next === 'models')
132
148
  return setStep({ id: 'modelList', provider: p });
133
149
  setStep({ id: 'model', provider: p });
134
- } })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
150
+ } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
151
+ ...step.provider.models.map((m) => ({
152
+ label: m,
153
+ value: m,
154
+ hint: m === step.provider.defaultModel ? t('wiz.model.default') : undefined,
155
+ })),
156
+ { label: t('set.back'), value: '__back__' },
157
+ ], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
158
+ if (v === '__back__') {
159
+ setStep(returnStep ?? { id: 'root' });
160
+ setReturnStep(null);
161
+ return;
162
+ }
163
+ pickModel(step.provider, v);
164
+ }, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
135
165
  ...step.provider.models.map((m) => ({
136
166
  label: m,
137
167
  value: m,
@@ -139,13 +169,17 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
139
169
  })),
140
170
  { label: t('set.back'), value: '__back__' },
141
171
  ], allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
142
- if (v === '__back__')
143
- return setStep({ id: 'root' });
172
+ if (v === '__back__') {
173
+ setStep(returnStep ?? { id: 'root' });
174
+ setReturnStep(null);
175
+ return;
176
+ }
144
177
  step.provider.defaultModel = v;
145
178
  ctl.saveProvider(step.provider);
146
179
  ctl.setDefaultProvider(step.provider.name);
147
180
  saved();
148
- setStep({ id: 'root' });
181
+ setStep(returnStep ?? { id: 'root' });
182
+ setReturnStep(null);
149
183
  }, onInput: (m) => {
150
184
  const model = m.trim();
151
185
  if (!model)
@@ -156,30 +190,46 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
156
190
  ctl.saveProvider(step.provider);
157
191
  ctl.setDefaultProvider(step.provider.name);
158
192
  saved();
159
- setStep({ id: 'root' });
193
+ setStep(returnStep ?? { id: 'root' });
194
+ setReturnStep(null);
160
195
  } })] })), step.id === 'priceModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
161
196
  ...step.provider.models.map((m) => {
162
197
  const pr = priceFor(step.provider, m);
163
198
  return {
164
199
  label: m,
165
200
  value: m,
166
- hint: pr ? `($${pr.input}/M in · $${pr.output}/M out${step.provider.prices?.[m] ? ' — override' : ''})` : `(${t('set.priceUnknown')})`,
201
+ hint: pr
202
+ ? `($${pr.input}/M in · $${pr.output}/M out${step.provider.prices?.[m] ? ' — override' : ''})`
203
+ : `(${t('set.priceUnknown')})`,
167
204
  };
168
205
  }),
169
206
  { label: t('set.back'), value: '__back__' },
170
207
  ], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
171
- if (v === '__back__')
172
- return setStep({ id: 'root' });
173
- setStep({ id: 'priceValue', provider: step.provider, model: v });
174
- }, onInput: (m) => setStep({ 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: [], allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
208
+ if (v === '__back__') {
209
+ setStep(returnStep ?? { id: 'root' });
210
+ setReturnStep(null);
211
+ return;
212
+ }
213
+ goSub({ id: 'priceValue', provider: step.provider, model: v });
214
+ }, 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: [], allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
175
215
  const m = v.match(/^\s*([\d.]+)\s*[,;\s]\s*([\d.]+)\s*$/);
176
216
  if (!m)
177
217
  return setFlash(t('set.priceBad'));
178
- step.provider.prices = { ...step.provider.prices, [step.model]: { input: parseFloat(m[1]), output: parseFloat(m[2]) } };
218
+ step.provider.prices = {
219
+ ...step.provider.prices,
220
+ [step.model]: { input: parseFloat(m[1]), output: parseFloat(m[2]) },
221
+ };
179
222
  ctl.saveProvider(step.provider);
180
223
  saved();
181
- setStep({ id: 'root' });
182
- } })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
224
+ setStep(returnStep ?? { id: 'root' });
225
+ setReturnStep(null);
226
+ } })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
227
+ step.provider.apiKey = k.trim();
228
+ ctl.saveProvider(step.provider);
229
+ saved();
230
+ setStep(returnStep ?? { id: 'root' });
231
+ setReturnStep(null);
232
+ } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], 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: [], 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: [], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => setStep({ id: 'newKey', name: step.name, url: step.url, model }) })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishNewProvider(step.name, step.url, step.model, key.trim()) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
183
233
  try {
184
234
  const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
185
235
  setFlash(t('m.skillCreated', { file }));
@@ -197,21 +247,86 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
197
247
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
198
248
  }
199
249
  setStep({ id: 'root' });
200
- } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
201
- ...step.provider.models.map((m) => ({
202
- label: m,
203
- value: m,
204
- hint: m === step.provider.defaultModel ? t('wiz.model.default') : undefined,
250
+ } })] })), 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: [
251
+ ...cfg.providers.map((p) => ({
252
+ label: p.name,
253
+ value: p.name,
254
+ hint: providerStatus(p, cfg.defaultProvider),
205
255
  })),
206
- { label: t('set.back'), value: '__back__' },
207
- ], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
256
+ { label: t('set.providers.add'), value: '__add__' },
257
+ { label: step.scope === 'global' ? t('set.providers.back') : t('sset.providers.back'), value: '__back__' },
258
+ ], onSelect: (v) => {
208
259
  if (v === '__back__')
209
260
  return setStep({ id: 'root' });
210
- pickModel(step.provider, v);
211
- }, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
212
- step.provider.apiKey = k.trim();
213
- ctl.saveProvider(step.provider);
214
- saved();
215
- setStep({ id: 'root' });
216
- } })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], 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: [], 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: [], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => setStep({ id: 'newKey', name: step.name, url: step.url, model }) })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishNewProvider(step.name, step.url, step.model, key.trim()) })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: t('set.esc') }) })] }));
261
+ if (v === '__add__') {
262
+ setReturnStep({ id: 'providers', scope: step.scope });
263
+ return setStep({ id: 'newName' });
264
+ }
265
+ const p = cfg.providers.find((x) => x.name === v);
266
+ if (!p)
267
+ return;
268
+ if (step.scope === 'session') {
269
+ // Session scope: pick a model for this session
270
+ setReturnStep({ id: 'root' });
271
+ setStep({ id: 'model', provider: p });
272
+ }
273
+ else {
274
+ // Global scope: go to provider detail
275
+ setStep({ id: 'providerDetail', provider: p, scope: 'global' });
276
+ }
277
+ } })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
278
+ {
279
+ label: t('set.providerDetail.key'),
280
+ value: 'key',
281
+ hint: masked(step.provider.apiKey),
282
+ },
283
+ {
284
+ label: t('set.providerDetail.models'),
285
+ value: 'models',
286
+ hint: `(${step.provider.models.length})`,
287
+ },
288
+ { label: t('set.providerDetail.pricing'), value: 'pricing' },
289
+ {
290
+ label: t('set.providerDetail.setDefault'),
291
+ value: 'setDefault',
292
+ hint: step.provider.name.toLowerCase() === cfg.defaultProvider.toLowerCase()
293
+ ? `(${t('set.status.default')})`
294
+ : undefined,
295
+ },
296
+ { label: t('set.providerDetail.remove'), value: 'remove' },
297
+ { label: t('set.providerDetail.back'), value: '__back__' },
298
+ ], onSelect: (v) => {
299
+ if (v === '__back__')
300
+ return setStep({ id: 'providers', scope: step.scope });
301
+ if (v === 'key') {
302
+ setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
303
+ return setStep({ id: 'key', provider: step.provider });
304
+ }
305
+ if (v === 'models') {
306
+ setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
307
+ return setStep({ id: 'modelList', provider: step.provider });
308
+ }
309
+ if (v === 'pricing') {
310
+ setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
311
+ return setStep({ id: 'priceModel', provider: step.provider });
312
+ }
313
+ if (v === 'setDefault') {
314
+ ctl.setDefaultProvider(step.provider.name);
315
+ saved();
316
+ return setStep({ id: 'providers', scope: step.scope });
317
+ }
318
+ if (v === 'remove')
319
+ return setStep({ id: 'removeProvider', provider: step.provider, scope: step.scope });
320
+ } })] })), 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: [
321
+ { label: t('set.removeProvider.yes'), value: 'yes' },
322
+ { label: t('set.removeProvider.no'), value: 'no' },
323
+ ], onSelect: (v) => {
324
+ if (v === 'no')
325
+ return setStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
326
+ if (v === 'yes') {
327
+ ctl.removeProvider(step.provider.name);
328
+ saved();
329
+ setStep({ id: 'providers', scope: step.scope });
330
+ }
331
+ } })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: t('set.esc') }) })] }));
217
332
  }
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { t } from '../i18n.js';
4
+ import { presentTimeline } from './events.js';
5
+ import { truncate } from './theme.js';
6
+ import { UI } from './tokens.js';
7
+ function sectionLabel(category) {
8
+ return t(`timeline.section.${category}`);
9
+ }
10
+ function fileLabel(label, count) {
11
+ const key = label === 'write' ? 'timeline.wroteFiles' : label === 'edit' ? 'timeline.editedFiles' : label === 'search' ? 'timeline.searched' : label === 'list' ? 'timeline.listed' : 'timeline.readFiles';
12
+ return t(key, { count });
13
+ }
14
+ function itemColor(item) {
15
+ if (item.status === 'error')
16
+ return UI.danger;
17
+ if (item.kind === 'command')
18
+ return UI.accent;
19
+ if (item.kind === 'files')
20
+ return item.category === 'change' ? UI.warn : UI.muted;
21
+ if (item.category === 'coordinate')
22
+ return UI.note;
23
+ if (item.kind === 'thought')
24
+ return UI.muted;
25
+ if (item.kind === 'narration')
26
+ return UI.text;
27
+ return UI.text;
28
+ }
29
+ function OutputLines({ item }) {
30
+ if (!item.output || item.output.length === 0)
31
+ return null;
32
+ return (_jsxs(Box, { flexDirection: "column", children: [item.output.map((line, i) => (_jsxs(Text, { color: item.status === 'error' ? UI.danger : UI.muted, wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: i === 0 ? '└ ' : ' ' }), truncate(line, 180)] }, `${item.seq ?? 0}-out-${i}`))), item.hiddenLines && item.hiddenLines > 0 ? (_jsxs(Text, { color: UI.muted, children: [' ', t('timeline.hiddenLines', { count: item.hiddenLines })] })) : null] }));
33
+ }
34
+ function TimelineRow({ item }) {
35
+ if (item.kind === 'section') {
36
+ return _jsx(Text, { color: UI.muted, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" });
37
+ }
38
+ if (item.kind === 'narration') {
39
+ return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: UI.text, wrap: "wrap", children: item.detail }) }));
40
+ }
41
+ if (item.kind === 'command') {
42
+ 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 ?? '', 160) })] }), _jsx(OutputLines, { item: item })] }));
43
+ }
44
+ if (item.kind === 'files') {
45
+ const files = item.files ?? [];
46
+ const shown = files.slice(0, 5).join(', ');
47
+ const extra = files.length > 5 ? ` +${files.length - 5}` : '';
48
+ return (_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), _jsxs(Text, { bold: true, children: [fileLabel(item.label, files.length), " "] }), _jsxs(Text, { color: UI.muted, children: [shown, extra] })] }));
49
+ }
50
+ if (item.output) {
51
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: itemColor(item), wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), item.label] }), _jsx(OutputLines, { item: item })] }));
52
+ }
53
+ return (_jsxs(Text, { color: itemColor(item), italic: item.kind === 'thought', wrap: "truncate-end", children: [_jsx(Text, { color: UI.muted, children: "\u2022 " }), truncate(item.detail ? `${item.label} ${item.detail}` : item.label, 180)] }));
54
+ }
55
+ export function Timeline({ logs, raw = false, emptyText }) {
56
+ const items = presentTimeline(logs, { raw, outputLines: raw ? 10 : 6 });
57
+ if (items.length === 0)
58
+ return _jsx(Text, { color: UI.muted, children: emptyText ?? t('timeline.empty') });
59
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => item.kind === 'section' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TimelineRow, { item: item }), _jsx(Text, { color: UI.muted, children: sectionLabel(item.category) })] }, `${item.seq ?? i}-section`)) : (_jsx(TimelineRow, { item: item }, `${item.seq ?? i}-${i}`))) }));
60
+ }
package/dist/ui/Wizard.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } 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';
@@ -12,6 +12,8 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
12
12
  const [typed, setTyped] = useState('');
13
13
  const typing = allowInput && typed.length > 0;
14
14
  useInput((input, key) => {
15
+ // Build selectable index list each render (cheap — items is small).
16
+ const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
15
17
  if (key.escape) {
16
18
  if (typed)
17
19
  setTyped('');
@@ -26,8 +28,10 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
26
28
  if (v)
27
29
  onInput?.(v);
28
30
  }
29
- else if (items[idx]) {
30
- onSelect?.(items[idx].value);
31
+ else {
32
+ const realIdx = selectable[idx];
33
+ if (realIdx !== undefined && items[realIdx])
34
+ onSelect?.(items[realIdx].value);
31
35
  }
32
36
  return;
33
37
  }
@@ -37,12 +41,12 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
37
41
  }
38
42
  if (key.upArrow) {
39
43
  if (!typing)
40
- setIdx((i) => (i - 1 + items.length) % Math.max(1, items.length));
44
+ setIdx((i) => (i - 1 + selectable.length) % Math.max(1, selectable.length));
41
45
  return;
42
46
  }
43
47
  if (key.downArrow) {
44
48
  if (!typing)
45
- setIdx((i) => (i + 1) % Math.max(1, items.length));
49
+ setIdx((i) => (i + 1) % Math.max(1, selectable.length));
46
50
  return;
47
51
  }
48
52
  if (key.tab || key.ctrl || key.meta)
@@ -59,7 +63,10 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
59
63
  }
60
64
  setTyped((v) => v + input);
61
65
  });
62
- return (_jsxs(Box, { flexDirection: "column", children: [items.map((it, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === idx ? 'cyanBright' : 'gray', bold: !typing && i === idx, children: [!typing && i === idx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null] }, it.value + i))), 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] }) }))] }));
66
+ // Build a separate index map so up/down skip section headers.
67
+ const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
68
+ const safeIdx = selectable.length > 0 ? selectable[Math.min(idx, selectable.length - 1)] : -1;
69
+ return (_jsxs(Box, { flexDirection: "column", children: [items.map((it, i) => 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))), 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] }) }))] }));
63
70
  }
64
71
  export function WizardStep({ step, total, title, children, footer, }) {
65
72
  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') }) })] }));