@parallel-cli/parallel 0.4.3 → 0.4.4
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 +35 -2
- package/README.md +77 -94
- package/dist/agents/agent.js +2 -2
- package/dist/agents/tools.js +47 -5
- package/dist/commands.js +104 -18
- package/dist/config.js +31 -1
- package/dist/controller.js +36 -7
- package/dist/i18n.js +132 -28
- package/dist/index.js +3 -3
- package/dist/ui/App.js +81 -60
- package/dist/ui/SettingsPanel.js +86 -18
- package/dist/ui/views.js +38 -15
- package/package.json +1 -1
package/dist/ui/App.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
6
6
|
import { Controller } from '../controller.js';
|
|
7
7
|
import { startSessionServer } from '../server.js';
|
|
8
8
|
import { executeInput } from '../commands.js';
|
|
9
|
-
import { PROVIDER_PRESETS, getProvider, providerNeedsApiKey, providerReady, rememberFolder, saveConfig } from '../config.js';
|
|
9
|
+
import { PROVIDER_PRESETS, detectProviderModels, getProvider, isLocalProvider, isPlaceholderModel, providerNeedsApiKey, providerReady, rememberFolder, saveConfig, } from '../config.js';
|
|
10
10
|
import { LANGS, setLang, t } from '../i18n.js';
|
|
11
11
|
import { AgentRow, AgentTranscript } from './AgentPanel.js';
|
|
12
12
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
@@ -18,7 +18,7 @@ import { SelectList, WizardStep } from './Wizard.js';
|
|
|
18
18
|
import { BRAND, CHROME, STATE, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
19
19
|
const LOGO = 'Parallel';
|
|
20
20
|
// Version from package.json. Hardcoded — rootDir: "src" prevents importing ../../package.json.
|
|
21
|
-
const VERSION = '0.4.
|
|
21
|
+
const VERSION = '0.4.4';
|
|
22
22
|
function usableProvider(config) {
|
|
23
23
|
const p = getProvider(config);
|
|
24
24
|
return p && providerReady(p) && (p.defaultModel || p.models[0]) ? p : undefined;
|
|
@@ -60,7 +60,7 @@ export function App({ config, initialFolder }) {
|
|
|
60
60
|
const [modelCustom, setModelCustom] = useState(false);
|
|
61
61
|
const ctlRef = useRef(directFolder ? new Controller(config, directFolder) : null);
|
|
62
62
|
// ---------- main state ----------
|
|
63
|
-
const [, setTick] = useState(0);
|
|
63
|
+
const [tick, setTick] = useState(0);
|
|
64
64
|
const [view, setView] = useState('agents');
|
|
65
65
|
// Focus mode (/focus <agent>): plain input is routed to that agent.
|
|
66
66
|
const [focus, setFocus] = useState(null);
|
|
@@ -107,6 +107,12 @@ export function App({ config, initialFolder }) {
|
|
|
107
107
|
clearInterval(timer);
|
|
108
108
|
};
|
|
109
109
|
}, [ctl]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!ctl || !focus)
|
|
112
|
+
return;
|
|
113
|
+
if (!ctl.board.getAgentByName(focus))
|
|
114
|
+
setFocus(null);
|
|
115
|
+
}, [ctl, focus, tick]);
|
|
110
116
|
// Session server: lets `parallel attach <agent>` open per-agent terminals.
|
|
111
117
|
useEffect(() => {
|
|
112
118
|
if (!ctl)
|
|
@@ -228,7 +234,10 @@ export function App({ config, initialFolder }) {
|
|
|
228
234
|
setProviderStep({ id: 'pick' });
|
|
229
235
|
}
|
|
230
236
|
else if (providerStep.id === 'endpoint') {
|
|
231
|
-
|
|
237
|
+
const isPreset = PROVIDER_PRESETS.some((p) => p.name.toLowerCase() === providerStep.provider.name.toLowerCase());
|
|
238
|
+
setProviderStep(isPreset
|
|
239
|
+
? { id: 'presetModel', provider: providerStep.provider }
|
|
240
|
+
: { id: 'customModel', name: providerStep.provider.name, url: providerStep.provider.baseUrl });
|
|
232
241
|
}
|
|
233
242
|
else if (providerStep.id === 'editEndpoint') {
|
|
234
243
|
setProviderStep({ id: 'endpoint', provider: providerStep.provider });
|
|
@@ -391,27 +400,16 @@ export function App({ config, initialFolder }) {
|
|
|
391
400
|
...ls.slice(-5),
|
|
392
401
|
{ text: t('wiz.provider.ollama.checking', { url: preset.baseUrl }), level: 'info' },
|
|
393
402
|
]);
|
|
394
|
-
|
|
395
|
-
let
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const data = (await resp.json());
|
|
403
|
-
const detected = data?.data?.map((m) => m.id).filter(Boolean) ?? [];
|
|
404
|
-
if (detected.length > 0) {
|
|
405
|
-
models = detected;
|
|
406
|
-
defaultModel = detected[0];
|
|
407
|
-
setSystemLines((ls) => [
|
|
408
|
-
...ls.slice(-5),
|
|
409
|
-
{ text: t('wiz.provider.ollama.found', { n: detected.length }), level: 'ok' },
|
|
410
|
-
]);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
403
|
+
const detected = await detectProviderModels(preset);
|
|
404
|
+
let models = detected?.models ?? [...preset.models];
|
|
405
|
+
let defaultModel = detected?.defaultModel ?? preset.defaultModel;
|
|
406
|
+
if (detected) {
|
|
407
|
+
setSystemLines((ls) => [
|
|
408
|
+
...ls.slice(-5),
|
|
409
|
+
{ text: t('wiz.provider.ollama.found', { n: detected.models.length }), level: 'ok' },
|
|
410
|
+
]);
|
|
413
411
|
}
|
|
414
|
-
|
|
412
|
+
else {
|
|
415
413
|
setSystemLines((ls) => [
|
|
416
414
|
...ls.slice(-5),
|
|
417
415
|
{ text: t('wiz.provider.ollama.notFound', { url: preset.baseUrl }), level: 'warn' },
|
|
@@ -452,16 +450,35 @@ export function App({ config, initialFolder }) {
|
|
|
452
450
|
if (providerNeedsApiKey(providerStep.provider))
|
|
453
451
|
return setProviderStep({ id: 'key', provider: providerStep.provider });
|
|
454
452
|
finishProvider(providerStep.provider);
|
|
455
|
-
} })] })), phase === 'provider' && providerStep.id === 'editEndpoint' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.url.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: providerStep.provider.baseUrl, onBack: wizardBack, onInput: (url) =>
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
453
|
+
} })] })), phase === 'provider' && providerStep.id === 'editEndpoint' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.url.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: providerStep.provider.baseUrl, onBack: wizardBack, onInput: (url) => {
|
|
454
|
+
const baseUrl = url.trim();
|
|
455
|
+
setProviderStep({
|
|
456
|
+
id: 'endpoint',
|
|
457
|
+
provider: {
|
|
458
|
+
...providerStep.provider,
|
|
459
|
+
baseUrl,
|
|
460
|
+
requiresApiKey: !isLocalProvider({ baseUrl }),
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
} }) })), phase === 'provider' && providerStep.id === 'key' && (_jsxs(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.provider.name }), footer: t('wiz.provider.key.footer'), children: [_jsx(Text, { color: "gray", children: providerStep.provider.baseUrl }), _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (k) => finishProvider({ ...providerStep.provider, apiKey: k.trim() }) })] })), phase === 'provider' && providerStep.id === 'name' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.name.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onBack: wizardBack, onInput: (name) => setProviderStep({ id: 'url', name }) }) })), phase === 'provider' && providerStep.id === 'url' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.url.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onBack: wizardBack, onInput: (url) => setProviderStep({ id: 'customModel', name: providerStep.name, url }) }) })), phase === 'provider' && providerStep.id === 'customModel' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.model.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onInput: (model) => {
|
|
464
|
+
const trimmed = model.trim();
|
|
465
|
+
if (isPlaceholderModel(trimmed)) {
|
|
466
|
+
setSystemLines((ls) => [...ls.slice(-5), { text: t('set.modelPlaceholder'), level: 'warn' }]);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const local = isLocalProvider({ baseUrl: providerStep.url });
|
|
470
|
+
setProviderStep({
|
|
471
|
+
id: 'endpoint',
|
|
472
|
+
provider: {
|
|
473
|
+
name: providerStep.name,
|
|
474
|
+
baseUrl: providerStep.url,
|
|
475
|
+
apiKey: '',
|
|
476
|
+
models: [trimmed],
|
|
477
|
+
defaultModel: trimmed,
|
|
478
|
+
requiresApiKey: !local,
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
} }) })), phase === 'provider' && providerStep.id === 'newKey' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.provider.name }), footer: t('wiz.provider.key.footer'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (key) => finishProvider({
|
|
465
482
|
...providerStep.provider,
|
|
466
483
|
apiKey: key.trim(),
|
|
467
484
|
}) }) })), phase === 'model' && !modelCustom && sessionProvider && (_jsxs(WizardStep, { step: 5, total: totalSteps, title: t('wiz.model.title'), children: [_jsx(Text, { color: "gray", children: t('wiz.model.provider', { name: sessionProvider.name, url: sessionProvider.baseUrl }) }), _jsx(SelectList, { items: [
|
|
@@ -548,8 +565,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
548
565
|
: [];
|
|
549
566
|
const [hubScroll, setHubScroll] = useState(0);
|
|
550
567
|
const [hubFollowTail, setHubFollowTail] = useState(true);
|
|
551
|
-
const hubRows = Math.max(
|
|
552
|
-
const maxHubScroll = Math.max(0, agents.length - hubRows);
|
|
568
|
+
const hubRows = Math.max(3, bodyHeight - 2);
|
|
569
|
+
const maxHubScroll = Math.max(0, agents.length - Math.max(1, Math.floor(hubRows / 2)));
|
|
553
570
|
const clampedHub = Math.min(hubScroll, maxHubScroll);
|
|
554
571
|
const logSeq = ctl.board.logs.length > 0 ? ctl.board.logs[ctl.board.logs.length - 1].seq ?? ctl.board.logs.length : 0;
|
|
555
572
|
useEffect(() => {
|
|
@@ -560,7 +577,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
560
577
|
if (hubFollowTail)
|
|
561
578
|
setHubScroll(0);
|
|
562
579
|
}, [logSeq, agents.length, hubFollowTail]);
|
|
563
|
-
// Scroll helpers
|
|
580
|
+
// Scroll helpers.
|
|
564
581
|
const scrollFocusUp = () => {
|
|
565
582
|
setFocusFollowTail(false);
|
|
566
583
|
setScroll((s) => Math.min(s + 1, maxScroll));
|
|
@@ -585,25 +602,23 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
585
602
|
return next;
|
|
586
603
|
});
|
|
587
604
|
};
|
|
588
|
-
// Keyboard:
|
|
589
|
-
// When CommandInput is NOT focused, Up/Down scroll the hub or focused agent;
|
|
590
|
-
// when it IS focused, CommandInput's own useInput sees them first (history nav).
|
|
605
|
+
// Keyboard: PgUp/PgDn always scroll hub/focus. Up/Down only scroll when input is inactive.
|
|
591
606
|
useInput((_input, key) => {
|
|
592
|
-
if (key.escape)
|
|
607
|
+
if (key.escape && !settingsOpen)
|
|
593
608
|
onEscape();
|
|
594
609
|
if (focused) {
|
|
595
|
-
if (key.pageUp || key.upArrow)
|
|
610
|
+
if (key.pageUp || (!inputActive && key.upArrow))
|
|
596
611
|
scrollFocusUp();
|
|
597
|
-
if (key.pageDown || key.downArrow)
|
|
612
|
+
if (key.pageDown || (!inputActive && key.downArrow))
|
|
598
613
|
scrollFocusDown();
|
|
599
614
|
}
|
|
600
615
|
else if (view === 'agents') {
|
|
601
|
-
if (key.pageUp || key.upArrow)
|
|
616
|
+
if (key.pageUp || (!inputActive && key.upArrow))
|
|
602
617
|
scrollHubUp();
|
|
603
|
-
if (key.pageDown || key.downArrow)
|
|
618
|
+
if (key.pageDown || (!inputActive && key.downArrow))
|
|
604
619
|
scrollHubDown();
|
|
605
620
|
}
|
|
606
|
-
}, { isActive: !
|
|
621
|
+
}, { isActive: !approval && !question });
|
|
607
622
|
const idleCount = agents.filter((a) => a.state === 'idle').length;
|
|
608
623
|
const workingCount = agents.filter((a) => ['working', 'thinking', 'listening'].includes(a.state)).length;
|
|
609
624
|
const doneCount = agents.filter((a) => a.state === 'done').length;
|
|
@@ -627,7 +642,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
627
642
|
specialists: 'specialists',
|
|
628
643
|
};
|
|
629
644
|
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
630
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CHROME.muted, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", viewLabel] }), rawLogs ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: agents.length > 0 ? 'space-between' : 'flex-end', children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : null, _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Text, { color: CHROME.muted, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Text, { children: " " }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills() })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists() })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight })) : agents.length === 0 ? (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
645
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CHROME.muted, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", viewLabel] }), rawLogs ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: agents.length > 0 ? 'space-between' : 'flex-end', children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : null, _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Text, { color: CHROME.muted, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Text, { children: " " }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight })) : agents.length === 0 ? (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
631
646
|
? systemLines
|
|
632
647
|
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
633
648
|
.slice(-2)
|
|
@@ -658,27 +673,33 @@ function groupAgents(agents) {
|
|
|
658
673
|
function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
|
|
659
674
|
const groups = groupAgents([...agents].sort((a, b) => STATE_META[a.state].rank - STATE_META[b.state].rank || a.startedAt - b.startedAt));
|
|
660
675
|
let skipped = scroll;
|
|
661
|
-
let
|
|
676
|
+
let renderedAgents = 0;
|
|
677
|
+
let renderedLines = 0;
|
|
662
678
|
const rows = [];
|
|
679
|
+
let full = false;
|
|
663
680
|
for (const group of groups) {
|
|
664
|
-
const groupRows = [];
|
|
665
681
|
for (const agent of group.agents) {
|
|
666
682
|
if (skipped > 0) {
|
|
667
683
|
skipped--;
|
|
668
684
|
continue;
|
|
669
685
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
686
|
+
const needsSeparator = rows.length > 0;
|
|
687
|
+
const neededLines = 2 + (needsSeparator ? 1 : 0);
|
|
688
|
+
if (renderedLines + neededLines > visibleRows) {
|
|
689
|
+
full = true;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
if (needsSeparator) {
|
|
693
|
+
rows.push(_jsx(Text, { color: CHROME.separator, children: '─'.repeat(cols - 2) }, `sep-${group.title}-${agent.id}`));
|
|
694
|
+
renderedLines++;
|
|
695
|
+
}
|
|
696
|
+
renderedAgents++;
|
|
697
|
+
renderedLines += 2;
|
|
698
|
+
rows.push(_jsx(AgentRow, { agent: agent, logs: ctl.board.logsFor(agent.id, 8), cols: cols }, agent.id));
|
|
679
699
|
}
|
|
680
|
-
|
|
700
|
+
if (full)
|
|
701
|
+
break;
|
|
681
702
|
}
|
|
682
|
-
const below = Math.max(0, agents.length - scroll -
|
|
703
|
+
const below = Math.max(0, agents.length - scroll - renderedAgents);
|
|
683
704
|
return (_jsxs(Box, { flexDirection: "column", children: [scroll > 0 ? _jsxs(Text, { color: CHROME.muted, children: ["\u25B2 ", scroll, " older \u00B7 PgDn to latest"] }) : null, rows, below > 0 ? _jsxs(Text, { color: CHROME.muted, children: ["\u25BC ", below, " more \u00B7 PgUp"] }) : null] }));
|
|
684
705
|
}
|
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/views.js
CHANGED
|
@@ -52,20 +52,27 @@ 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
|
+
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, 110)] })] }, 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)))] }));
|
|
59
64
|
}
|
|
60
|
-
export function NotesView({ board }) {
|
|
61
|
-
const
|
|
65
|
+
export function NotesView({ board, bodyHeight }) {
|
|
66
|
+
const fallbackVisible = useVisibleRows(7);
|
|
67
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 4) : fallbackVisible;
|
|
62
68
|
const { slice, above, below } = useScrollWindow(board.notes, visible, 'bottom');
|
|
63
69
|
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
70
|
}
|
|
65
|
-
export function DiffView({ board }) {
|
|
71
|
+
export function DiffView({ board, bodyHeight }) {
|
|
66
72
|
// Each change renders up to ~33 rows (header + 30 patch lines + spacing):
|
|
67
73
|
// window over WHOLE history, newest first, PgUp to walk back in time.
|
|
68
|
-
const
|
|
74
|
+
const fallbackRows = useVisibleRows(8, 18);
|
|
75
|
+
const rows = bodyHeight ? Math.max(8, bodyHeight - 4) : fallbackRows;
|
|
69
76
|
const perChange = Math.max(1, Math.floor(rows / 34));
|
|
70
77
|
const { slice: changes, above, below } = useScrollWindow(board.changes, perChange, 'bottom');
|
|
71
78
|
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 +82,40 @@ export function DiffView({ board }) {
|
|
|
75
82
|
}), _jsx(Below, { n: below })] }))] }));
|
|
76
83
|
}
|
|
77
84
|
/** Financial view: live cost / steps / tokens per agent + session total. */
|
|
78
|
-
export function CostView({ board }) {
|
|
85
|
+
export function CostView({ board, bodyHeight }) {
|
|
79
86
|
const agents = [...board.agents.values()];
|
|
87
|
+
const fallbackVisible = useVisibleRows(8);
|
|
88
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 7) : fallbackVisible;
|
|
89
|
+
const { slice, above, below } = useScrollWindow(agents, visible, 'top');
|
|
80
90
|
const total = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
|
|
81
91
|
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: [
|
|
92
|
+
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
93
|
}
|
|
84
94
|
/** Skills catalog: user-authored markdown instructions agents can load. */
|
|
85
|
-
export function SkillsView({ skills }) {
|
|
86
|
-
|
|
95
|
+
export function SkillsView({ skills, bodyHeight }) {
|
|
96
|
+
const fallbackVisible = useVisibleRows(8);
|
|
97
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
|
|
98
|
+
const { slice, above, below } = useScrollWindow(skills, visible, 'top');
|
|
99
|
+
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
100
|
}
|
|
88
101
|
/** Specialists catalog: personas (role + optional pinned model). */
|
|
89
|
-
export function SpecialistsView({ specialists }) {
|
|
90
|
-
|
|
102
|
+
export function SpecialistsView({ specialists, bodyHeight }) {
|
|
103
|
+
const fallbackVisible = useVisibleRows(8);
|
|
104
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 6) : fallbackVisible;
|
|
105
|
+
const { slice, above, below } = useScrollWindow(specialists, visible, 'top');
|
|
106
|
+
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
107
|
}
|
|
92
108
|
/** Saved sessions: inspect available restore points; restore via /session. */
|
|
93
|
-
export function SessionsView({ projectRoot }) {
|
|
109
|
+
export function SessionsView({ projectRoot, bodyHeight }) {
|
|
94
110
|
const sessions = Controller.listSessions(projectRoot);
|
|
95
|
-
|
|
111
|
+
const fallbackVisible = useVisibleRows(7);
|
|
112
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 5) : fallbackVisible;
|
|
113
|
+
const { slice, above, below } = useScrollWindow(sessions, visible, 'top');
|
|
114
|
+
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', {
|
|
115
|
+
name: s.data.name ? `${s.data.name} · ` : '',
|
|
116
|
+
date: new Date(s.data.savedAt).toLocaleString(),
|
|
117
|
+
agents: s.data.agents.length,
|
|
118
|
+
}) }), _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
119
|
}
|
|
97
120
|
export function HelpView({ bodyHeight }) {
|
|
98
121
|
// Fixed intro/highlight/footer rows consume about 12 lines inside the already-sized body.
|
package/package.json
CHANGED