@parallel-cli/parallel 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -2
- package/README.md +112 -99
- package/dist/agents/agent.js +2 -2
- package/dist/agents/tools.js +105 -7
- package/dist/commands.js +104 -18
- package/dist/config.js +31 -1
- package/dist/controller.js +68 -14
- package/dist/coordination/blackboard.js +75 -0
- package/dist/i18n.js +140 -32
- package/dist/index.js +3 -3
- package/dist/server.js +10 -0
- package/dist/ui/AgentPanel.js +22 -7
- package/dist/ui/App.js +86 -62
- package/dist/ui/AttachApp.js +13 -3
- package/dist/ui/CommandInput.js +60 -9
- package/dist/ui/SettingsPanel.js +86 -18
- package/dist/ui/Timeline.js +11 -9
- package/dist/ui/views.js +39 -15
- package/package.json +1 -1
package/dist/ui/SettingsPanel.js
CHANGED
|
@@ -3,9 +3,9 @@ import { 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';
|
|
6
|
-
import { SelectList } from './Wizard.js';
|
|
6
|
+
import { SelectList as BaseSelectList } from './Wizard.js';
|
|
7
7
|
import { LANGS, getLang, setLang, t } from '../i18n.js';
|
|
8
|
-
import { providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
|
|
8
|
+
import { detectProviderModels, isLocalProvider, isPlaceholderModel, providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
|
|
9
9
|
function masked(key) {
|
|
10
10
|
if (!key)
|
|
11
11
|
return '—';
|
|
@@ -39,6 +39,16 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
|
39
39
|
const saved = () => setFlash(t('set.saved'));
|
|
40
40
|
const cfg = ctl.config;
|
|
41
41
|
const listHeight = height ? Math.max(3, height - 5) : undefined;
|
|
42
|
+
const goBack = () => {
|
|
43
|
+
if (step.id === 'root')
|
|
44
|
+
return onClose();
|
|
45
|
+
setStep(returnStep ?? { id: 'root' });
|
|
46
|
+
setReturnStep(null);
|
|
47
|
+
};
|
|
48
|
+
const SelectList = (props) => {
|
|
49
|
+
const { onBack, ...rest } = props;
|
|
50
|
+
return _jsx(BaseSelectList, { ...rest, onBack: onBack ?? goBack });
|
|
51
|
+
};
|
|
42
52
|
// ---- root menu items ----
|
|
43
53
|
const rootItems = scope === 'global'
|
|
44
54
|
? [
|
|
@@ -103,6 +113,10 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
|
103
113
|
};
|
|
104
114
|
// ---- shared helpers ----
|
|
105
115
|
const pickModel = (provider, model) => {
|
|
116
|
+
if (isPlaceholderModel(model)) {
|
|
117
|
+
setFlash(t('set.modelPlaceholder'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
106
120
|
if (step.id === 'model' && step.setup) {
|
|
107
121
|
provider.defaultModel = model;
|
|
108
122
|
if (!provider.models.includes(model))
|
|
@@ -124,23 +138,25 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
|
124
138
|
setStep(returnStep ?? { id: 'root' });
|
|
125
139
|
setReturnStep(null);
|
|
126
140
|
};
|
|
127
|
-
const finishProviderSetup = (provider) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
const finishProviderSetup = (provider, persist = scope === 'global') => {
|
|
142
|
+
if (persist) {
|
|
143
|
+
ctl.saveProvider(provider);
|
|
144
|
+
if (scope === 'global') {
|
|
145
|
+
ctl.setDefaultProvider(provider.name);
|
|
146
|
+
saved();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
ctl.setSessionModel(`${provider.name}:${provider.defaultModel || provider.models[0] || ''}`);
|
|
150
|
+
setFlash(t('set.saved'));
|
|
151
|
+
}
|
|
132
152
|
}
|
|
133
153
|
else {
|
|
134
|
-
ctl.
|
|
154
|
+
ctl.setSessionProviderConfig(provider);
|
|
155
|
+
setFlash(t('set.sessionProviderReady', { name: provider.name }));
|
|
135
156
|
}
|
|
136
157
|
setStep(returnStep ?? { id: 'providers', scope });
|
|
137
158
|
setReturnStep(null);
|
|
138
159
|
};
|
|
139
|
-
const finishNewProvider = (name, url, model, key) => {
|
|
140
|
-
finishProviderSetup({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
|
|
141
|
-
setStep(returnStep ?? { id: 'root' });
|
|
142
|
-
setReturnStep(null);
|
|
143
|
-
};
|
|
144
160
|
// ---- navigate into a sub-step, remembering where to return ----
|
|
145
161
|
const goSub = (next) => {
|
|
146
162
|
setReturnStep(step);
|
|
@@ -194,6 +210,8 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
|
194
210
|
if (step.setup) {
|
|
195
211
|
if (providerNeedsApiKey(step.provider))
|
|
196
212
|
return setStep({ id: 'key', provider: step.provider, setup: true });
|
|
213
|
+
if (scope === 'session')
|
|
214
|
+
return setStep({ id: 'setupScope', provider: step.provider });
|
|
197
215
|
finishProviderSetup(step.provider);
|
|
198
216
|
return;
|
|
199
217
|
}
|
|
@@ -201,6 +219,14 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
|
201
219
|
saved();
|
|
202
220
|
setStep(returnStep ?? { id: 'providerDetail', provider: step.provider, scope });
|
|
203
221
|
setReturnStep(null);
|
|
222
|
+
} })] })), step.id === 'setupScope' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.setupScope.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
|
|
223
|
+
{ label: t('set.setupScope.session'), value: 'session' },
|
|
224
|
+
{ label: t('set.setupScope.global'), value: 'global' },
|
|
225
|
+
{ label: t('set.back'), value: '__back__' },
|
|
226
|
+
], height: listHeight, onSelect: (v) => {
|
|
227
|
+
if (v === '__back__')
|
|
228
|
+
return setStep({ id: 'endpoint', provider: step.provider, setup: true });
|
|
229
|
+
finishProviderSetup(step.provider, v === 'global');
|
|
204
230
|
} })] })), 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) => {
|
|
205
231
|
const provider = { ...step.provider, baseUrl: url.trim() };
|
|
206
232
|
setStep({ id: 'endpoint', provider, setup: step.setup });
|
|
@@ -268,15 +294,40 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
|
268
294
|
setReturnStep(null);
|
|
269
295
|
} })] })), 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) => {
|
|
270
296
|
const provider = { ...step.provider, apiKey: k.trim() };
|
|
271
|
-
if (step.setup)
|
|
297
|
+
if (step.setup) {
|
|
298
|
+
if (scope === 'session') {
|
|
299
|
+
setStep({ id: 'setupScope', provider });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
272
302
|
finishProviderSetup(provider);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
273
305
|
else {
|
|
274
306
|
ctl.saveProvider(provider);
|
|
275
307
|
saved();
|
|
276
308
|
}
|
|
277
309
|
setStep(returnStep ?? { id: 'root' });
|
|
278
310
|
setReturnStep(null);
|
|
279
|
-
} })] })), 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(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) => {
|
|
312
|
+
const trimmed = model.trim();
|
|
313
|
+
if (isPlaceholderModel(trimmed)) {
|
|
314
|
+
setFlash(t('set.modelPlaceholder'));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const local = isLocalProvider({ baseUrl: step.url });
|
|
318
|
+
setStep({
|
|
319
|
+
id: 'endpoint',
|
|
320
|
+
setup: true,
|
|
321
|
+
provider: {
|
|
322
|
+
name: step.name,
|
|
323
|
+
baseUrl: step.url,
|
|
324
|
+
apiKey: '',
|
|
325
|
+
models: [trimmed],
|
|
326
|
+
defaultModel: trimmed,
|
|
327
|
+
requiresApiKey: !local,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
} })] })), 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) => {
|
|
280
331
|
try {
|
|
281
332
|
const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
|
|
282
333
|
setFlash(t('m.skillCreated', { file }));
|
|
@@ -355,9 +406,26 @@ export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
|
355
406
|
const preset = PROVIDER_PRESETS.find((p) => p.name === presetName);
|
|
356
407
|
if (!preset)
|
|
357
408
|
return;
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
409
|
+
if (preset.category === 'local') {
|
|
410
|
+
setFlash(t('wiz.provider.ollama.checking', { url: preset.baseUrl }));
|
|
411
|
+
void (async () => {
|
|
412
|
+
const detected = await detectProviderModels(preset);
|
|
413
|
+
setFlash(detected
|
|
414
|
+
? t('wiz.provider.ollama.found', { n: detected.models.length })
|
|
415
|
+
: t('wiz.provider.ollama.notFound', { url: preset.baseUrl }));
|
|
416
|
+
setReturnStep({ id: 'providers', scope: step.scope });
|
|
417
|
+
setStep({
|
|
418
|
+
id: 'model',
|
|
419
|
+
provider: {
|
|
420
|
+
...preset,
|
|
421
|
+
apiKey: 'local',
|
|
422
|
+
models: detected?.models ?? [...preset.models],
|
|
423
|
+
defaultModel: detected?.defaultModel ?? preset.defaultModel,
|
|
424
|
+
},
|
|
425
|
+
setup: true,
|
|
426
|
+
});
|
|
427
|
+
})();
|
|
428
|
+
return;
|
|
361
429
|
}
|
|
362
430
|
// Preset setup: choose model, review endpoint, then ask for API key if needed.
|
|
363
431
|
setReturnStep({ id: 'providers', scope: step.scope });
|
package/dist/ui/Timeline.js
CHANGED
|
@@ -26,20 +26,22 @@ function itemColor(item) {
|
|
|
26
26
|
return UI.text;
|
|
27
27
|
return UI.text;
|
|
28
28
|
}
|
|
29
|
-
function OutputLines({ item }) {
|
|
29
|
+
function OutputLines({ item, cols }) {
|
|
30
30
|
if (!item.output || item.output.length === 0)
|
|
31
31
|
return null;
|
|
32
|
-
|
|
32
|
+
const max = Math.max(40, cols - 8);
|
|
33
|
+
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, max)] }, `${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
|
}
|
|
34
|
-
function TimelineRow({ item }) {
|
|
35
|
+
function TimelineRow({ item, cols }) {
|
|
36
|
+
const max = Math.max(40, cols - 8);
|
|
35
37
|
if (item.kind === 'section') {
|
|
36
|
-
return _jsx(Text, { color: UI.muted, children:
|
|
38
|
+
return _jsx(Text, { color: UI.muted, children: '─'.repeat(Math.min(Math.max(20, cols - 4), 80)) });
|
|
37
39
|
}
|
|
38
40
|
if (item.kind === 'narration') {
|
|
39
41
|
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: UI.text, wrap: "wrap", children: item.detail }) }));
|
|
40
42
|
}
|
|
41
43
|
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 ?? '',
|
|
44
|
+
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 })] }));
|
|
43
45
|
}
|
|
44
46
|
if (item.kind === 'files') {
|
|
45
47
|
const files = item.files ?? [];
|
|
@@ -48,13 +50,13 @@ function TimelineRow({ item }) {
|
|
|
48
50
|
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
51
|
}
|
|
50
52
|
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 })] }));
|
|
53
|
+
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, cols: cols })] }));
|
|
52
54
|
}
|
|
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,
|
|
55
|
+
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, max)] }));
|
|
54
56
|
}
|
|
55
|
-
export function Timeline({ logs, raw = false, emptyText }) {
|
|
57
|
+
export function Timeline({ logs, raw = false, emptyText, cols = 100 }) {
|
|
56
58
|
const items = presentTimeline(logs, { raw, outputLines: raw ? 10 : 6 });
|
|
57
59
|
if (items.length === 0)
|
|
58
60
|
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}`))) }));
|
|
61
|
+
return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => item.kind === 'section' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(TimelineRow, { item: item, cols: cols }), _jsx(Text, { color: UI.muted, children: sectionLabel(item.category) })] }, `${item.seq ?? i}-section`)) : (_jsx(TimelineRow, { item: item, cols: cols }, `${item.seq ?? i}-${i}`))) }));
|
|
60
62
|
}
|
package/dist/ui/views.js
CHANGED
|
@@ -52,20 +52,28 @@ function useVisibleRows(overhead, min = 6) {
|
|
|
52
52
|
const { stdout } = useStdout();
|
|
53
53
|
return Math.max(min, (stdout?.rows ?? 30) - overhead);
|
|
54
54
|
}
|
|
55
|
-
export function BoardView({ board }) {
|
|
55
|
+
export function BoardView({ board, bodyHeight }) {
|
|
56
56
|
const agents = [...board.agents.values()];
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const fallbackVisible = useVisibleRows(12);
|
|
58
|
+
const visibleAgents = bodyHeight ? Math.max(1, Math.floor((bodyHeight - 7) / 3)) : fallbackVisible;
|
|
59
|
+
const { slice: agentSlice, above, below } = useScrollWindow(agents, visibleAgents, 'top');
|
|
60
|
+
const sideRows = bodyHeight ? Math.max(1, Math.floor((bodyHeight - visibleAgents - 5) / 2)) : 8;
|
|
61
|
+
const activities = [...board.fileActivity.values()].sort((a, b) => b.ts - a.ts).slice(0, sideRows);
|
|
62
|
+
const notes = board.notes.slice(-sideRows);
|
|
63
|
+
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)))] }));
|
|
59
65
|
}
|
|
60
|
-
export function NotesView({ board }) {
|
|
61
|
-
const
|
|
66
|
+
export function NotesView({ board, bodyHeight }) {
|
|
67
|
+
const fallbackVisible = useVisibleRows(7);
|
|
68
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 4) : fallbackVisible;
|
|
62
69
|
const { slice, above, below } = useScrollWindow(board.notes, visible, 'bottom');
|
|
63
70
|
return (_jsxs(Box, { borderStyle: "round", borderColor: "magenta", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "magenta", children: t('notes.title') }), board.notes.length === 0 ? (_jsx(Text, { color: "gray", children: t('notes.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((n) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: "gray", children: [new Date(n.ts).toLocaleTimeString(), " "] }), _jsx(Text, { color: "magenta", bold: true, children: n.from }), _jsxs(Text, { color: "gray", children: [" \u2192 ", n.to, ": "] }), _jsx(Text, { children: truncate(n.content, 200) })] }, n.id))), _jsx(Below, { n: below })] }))] }));
|
|
64
71
|
}
|
|
65
|
-
export function DiffView({ board }) {
|
|
72
|
+
export function DiffView({ board, bodyHeight }) {
|
|
66
73
|
// Each change renders up to ~33 rows (header + 30 patch lines + spacing):
|
|
67
74
|
// window over WHOLE history, newest first, PgUp to walk back in time.
|
|
68
|
-
const
|
|
75
|
+
const fallbackRows = useVisibleRows(8, 18);
|
|
76
|
+
const rows = bodyHeight ? Math.max(8, bodyHeight - 4) : fallbackRows;
|
|
69
77
|
const perChange = Math.max(1, Math.floor(rows / 34));
|
|
70
78
|
const { slice: changes, above, below } = useScrollWindow(board.changes, perChange, 'bottom');
|
|
71
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) => {
|
|
@@ -75,24 +83,40 @@ export function DiffView({ board }) {
|
|
|
75
83
|
}), _jsx(Below, { n: below })] }))] }));
|
|
76
84
|
}
|
|
77
85
|
/** Financial view: live cost / steps / tokens per agent + session total. */
|
|
78
|
-
export function CostView({ board }) {
|
|
86
|
+
export function CostView({ board, bodyHeight }) {
|
|
79
87
|
const agents = [...board.agents.values()];
|
|
88
|
+
const fallbackVisible = useVisibleRows(8);
|
|
89
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 7) : fallbackVisible;
|
|
90
|
+
const { slice, above, below } = useScrollWindow(agents, visible, 'top');
|
|
80
91
|
const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
|
|
81
92
|
const unknown = agents.some((a) => a.cost === null);
|
|
82
|
-
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: [
|
|
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') })] }));
|
|
83
94
|
}
|
|
84
95
|
/** Skills catalog: user-authored markdown instructions agents can load. */
|
|
85
|
-
export function SkillsView({ skills }) {
|
|
86
|
-
|
|
96
|
+
export function SkillsView({ skills, bodyHeight }) {
|
|
97
|
+
const fallbackVisible = useVisibleRows(8);
|
|
98
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
|
|
99
|
+
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') })] }));
|
|
87
101
|
}
|
|
88
102
|
/** Specialists catalog: personas (role + optional pinned model). */
|
|
89
|
-
export function SpecialistsView({ specialists }) {
|
|
90
|
-
|
|
103
|
+
export function SpecialistsView({ specialists, bodyHeight }) {
|
|
104
|
+
const fallbackVisible = useVisibleRows(8);
|
|
105
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
|
|
106
|
+
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') })] }));
|
|
91
108
|
}
|
|
92
109
|
/** Saved sessions: inspect available restore points; restore via /session. */
|
|
93
|
-
export function SessionsView({ projectRoot }) {
|
|
110
|
+
export function SessionsView({ projectRoot, bodyHeight }) {
|
|
94
111
|
const sessions = Controller.listSessions(projectRoot);
|
|
95
|
-
|
|
112
|
+
const fallbackVisible = useVisibleRows(7);
|
|
113
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 5) : fallbackVisible;
|
|
114
|
+
const { slice, above, below } = useScrollWindow(sessions, visible, 'top');
|
|
115
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('sessions.title') }), sessions.length === 0 ? (_jsx(Text, { color: "gray", children: t('sessions.empty') })) : (_jsxs(_Fragment, { children: [_jsx(Above, { n: above }), slice.map((s, i) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "yellow", bold: true, children: [String(sessions.indexOf(s) + 1).padStart(2), "."] }), ' ', _jsx(Text, { children: t('sessions.item', {
|
|
116
|
+
name: s.data.name ? `${s.data.name} · ` : '',
|
|
117
|
+
date: new Date(s.data.savedAt).toLocaleString(),
|
|
118
|
+
agents: s.data.agents.length,
|
|
119
|
+
}) }), _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') })] }));
|
|
96
120
|
}
|
|
97
121
|
export function HelpView({ bodyHeight }) {
|
|
98
122
|
// Fixed intro/highlight/footer rows consume about 12 lines inside the already-sized body.
|
package/package.json
CHANGED