@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.
- package/README.md +156 -82
- package/dist/agents/agent.js +24 -2
- package/dist/agents/tools.js +4 -2
- package/dist/commands.js +179 -135
- package/dist/config.js +79 -0
- package/dist/controller.js +58 -5
- package/dist/i18n.js +304 -40
- package/dist/index.js +4 -2
- package/dist/pricing.js +27 -0
- package/dist/server.js +2 -1
- package/dist/ui/AgentPanel.js +85 -16
- package/dist/ui/App.js +285 -86
- package/dist/ui/AttachApp.js +46 -21
- package/dist/ui/CommandInput.js +56 -15
- package/dist/ui/SettingsPanel.js +170 -55
- package/dist/ui/Timeline.js +60 -0
- package/dist/ui/Wizard.js +13 -6
- package/dist/ui/events.js +229 -0
- package/dist/ui/theme.js +5 -4
- package/dist/ui/tokens.js +77 -0
- package/dist/ui/views.js +9 -3
- package/package.json +2 -2
package/dist/ui/App.js
CHANGED
|
@@ -7,16 +7,18 @@ import { Controller } from '../controller.js';
|
|
|
7
7
|
import { startSessionServer } from '../server.js';
|
|
8
8
|
import { executeInput } from '../commands.js';
|
|
9
9
|
import { PROVIDER_PRESETS, getProvider, rememberFolder, saveConfig } from '../config.js';
|
|
10
|
-
import { fmtCost } from '../pricing.js';
|
|
11
10
|
import { LANGS, setLang, t } from '../i18n.js';
|
|
12
|
-
import {
|
|
11
|
+
import { AgentRow, AgentTranscript } from './AgentPanel.js';
|
|
13
12
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
14
13
|
import { QuestionPrompt } from './QuestionPrompt.js';
|
|
15
14
|
import { CommandInput } from './CommandInput.js';
|
|
16
15
|
import { SettingsPanel } from './SettingsPanel.js';
|
|
17
16
|
import { BoardView, CostView, DiffView, HelpView, NotesView, SessionsView, SkillsView, SpecialistsView } from './views.js';
|
|
18
17
|
import { SelectList, WizardStep } from './Wizard.js';
|
|
19
|
-
|
|
18
|
+
import { BRAND, CHROME, STATE, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
19
|
+
const LOGO = 'Parallel';
|
|
20
|
+
// Version from package.json (v0.3.3). Hardcoded — rootDir: "src" prevents importing ../../package.json.
|
|
21
|
+
const VERSION = '0.3.3';
|
|
20
22
|
function usableProvider(config) {
|
|
21
23
|
const p = getProvider(config);
|
|
22
24
|
return p && p.apiKey && (p.defaultModel || p.models[0]) ? p : undefined;
|
|
@@ -60,7 +62,13 @@ export function App({ config, initialFolder }) {
|
|
|
60
62
|
const [view, setView] = useState('agents');
|
|
61
63
|
// Focus mode (/focus <agent>): plain input is routed to that agent.
|
|
62
64
|
const [focus, setFocus] = useState(null);
|
|
63
|
-
const [
|
|
65
|
+
const [rawLogs, setRawLogs] = useState(false);
|
|
66
|
+
const [systemLines, setSystemLines] = useState(directFolder
|
|
67
|
+
? [
|
|
68
|
+
{ text: t('main.ready1', { folder: directFolder }), level: 'ok' },
|
|
69
|
+
{ text: t('main.ready2'), level: 'info' },
|
|
70
|
+
]
|
|
71
|
+
: []);
|
|
64
72
|
const [inputReady, setInputReady] = useState(Boolean(directFolder));
|
|
65
73
|
const ctl = ctlRef.current;
|
|
66
74
|
// Re-render (throttled) on every blackboard/controller update.
|
|
@@ -97,11 +105,33 @@ export function App({ config, initialFolder }) {
|
|
|
97
105
|
}, [ctl]);
|
|
98
106
|
const ui = useMemo(() => ({
|
|
99
107
|
setView,
|
|
100
|
-
system: (line) => setSystemLines((ls) => [...ls.slice(-5), line]),
|
|
108
|
+
system: (line, level) => setSystemLines((ls) => [...ls.slice(-5), { text: line, level }]),
|
|
101
109
|
exit: () => {
|
|
102
110
|
setTimeout(() => exit(), 50);
|
|
103
111
|
},
|
|
104
112
|
setFocus,
|
|
113
|
+
toggleRaw: () => setRawLogs((v) => {
|
|
114
|
+
const next = !v;
|
|
115
|
+
setSystemLines((ls) => [
|
|
116
|
+
...ls.slice(-5),
|
|
117
|
+
{ text: t(next ? 'm.rawOn' : 'm.rawOff'), level: 'info' },
|
|
118
|
+
]);
|
|
119
|
+
return next;
|
|
120
|
+
}),
|
|
121
|
+
copyLatest: () => {
|
|
122
|
+
const agents = [...(ctlRef.current?.board.agents.values() ?? [])].filter((a) => a.lastResult);
|
|
123
|
+
const latest = agents.sort((a, b) => b.startedAt - a.startedAt)[0];
|
|
124
|
+
if (!latest?.lastResult) {
|
|
125
|
+
setSystemLines((ls) => [...ls.slice(-5), { text: t('m.copyNone'), level: 'warn' }]);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const encoded = Buffer.from(latest.lastResult).toString('base64');
|
|
129
|
+
process.stdout.write(`\x1b]52;c;${encoded}\x07`);
|
|
130
|
+
setSystemLines((ls) => [
|
|
131
|
+
...ls.slice(-5),
|
|
132
|
+
{ text: t('m.copyDone', { name: latest.name }), level: 'ok' },
|
|
133
|
+
]);
|
|
134
|
+
},
|
|
105
135
|
}), [exit]);
|
|
106
136
|
// ---------- wizard transitions ----------
|
|
107
137
|
// In normal launches, a complete config goes straight to the main TUI.
|
|
@@ -195,7 +225,10 @@ export function App({ config, initialFolder }) {
|
|
|
195
225
|
enterMain();
|
|
196
226
|
};
|
|
197
227
|
const enterMain = () => {
|
|
198
|
-
setSystemLines([
|
|
228
|
+
setSystemLines([
|
|
229
|
+
{ text: t('main.ready1', { folder }), level: 'ok' },
|
|
230
|
+
{ text: t('main.ready2'), level: 'info' },
|
|
231
|
+
]);
|
|
199
232
|
setPhase('main');
|
|
200
233
|
setInputReady(false);
|
|
201
234
|
setTimeout(() => setInputReady(true), 350);
|
|
@@ -241,37 +274,106 @@ export function App({ config, initialFolder }) {
|
|
|
241
274
|
.join(', ')
|
|
242
275
|
.slice(0, 60)})`,
|
|
243
276
|
})),
|
|
244
|
-
], onBack: wizardBack, onSelect: chooseSession }) })), phase === 'provider' && providerStep.id === 'pick' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.title'), children: _jsx(SelectList, { items:
|
|
245
|
-
|
|
277
|
+
], onBack: wizardBack, onSelect: chooseSession }) })), phase === 'provider' && providerStep.id === 'pick' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.title'), children: _jsx(SelectList, { items: (() => {
|
|
278
|
+
const items = [];
|
|
279
|
+
// Section: Configured
|
|
280
|
+
const configured = config.providers.map((p) => ({
|
|
246
281
|
label: p.name,
|
|
247
|
-
value:
|
|
248
|
-
|
|
249
|
-
}))
|
|
250
|
-
|
|
251
|
-
label:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
282
|
+
value: p.name,
|
|
283
|
+
detail: p.apiKey ? undefined : t('wiz.provider.needsKey'),
|
|
284
|
+
}));
|
|
285
|
+
if (configured.length > 0) {
|
|
286
|
+
items.push({ label: t('wiz.provider.section.configured'), value: '', section: true });
|
|
287
|
+
items.push(...configured);
|
|
288
|
+
}
|
|
289
|
+
// Section: Cloud Providers (presets except Ollama, not already configured)
|
|
290
|
+
const cloudPresets = PROVIDER_PRESETS.filter((preset) => preset.name !== 'Ollama' &&
|
|
291
|
+
!config.providers.some((p) => p.name.toLowerCase() === preset.name.toLowerCase()));
|
|
292
|
+
if (cloudPresets.length > 0) {
|
|
293
|
+
items.push({ label: t('wiz.provider.section.cloud'), value: '', section: true });
|
|
294
|
+
items.push(...cloudPresets.map((preset) => ({
|
|
295
|
+
label: preset.name,
|
|
296
|
+
value: preset.name,
|
|
297
|
+
detail: preset.defaultModel,
|
|
298
|
+
})));
|
|
299
|
+
}
|
|
300
|
+
// Section: Local (Ollama only, if not already configured)
|
|
301
|
+
const ollamaPreset = PROVIDER_PRESETS.find((p) => p.name === 'Ollama');
|
|
302
|
+
const ollamaConfigured = config.providers.some((p) => p.name.toLowerCase() === 'ollama');
|
|
303
|
+
if (ollamaPreset && !ollamaConfigured) {
|
|
304
|
+
items.push({ label: t('wiz.provider.section.local'), value: '', section: true });
|
|
305
|
+
items.push({
|
|
306
|
+
label: ollamaPreset.name,
|
|
307
|
+
value: ollamaPreset.name,
|
|
308
|
+
detail: t('wiz.provider.ollamaDetail'),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Custom always last
|
|
312
|
+
items.push({
|
|
313
|
+
label: t('wiz.provider.custom'),
|
|
314
|
+
value: '__custom__',
|
|
315
|
+
detail: t('wiz.provider.customDetail'),
|
|
316
|
+
});
|
|
317
|
+
return items;
|
|
318
|
+
})(), onBack: wizardBack, onSelect: async (v) => {
|
|
257
319
|
if (v === '__custom__')
|
|
258
320
|
return setProviderStep({ id: 'name' });
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
321
|
+
// Already-configured provider?
|
|
322
|
+
const existing = config.providers.find((x) => x.name === v);
|
|
323
|
+
if (existing) {
|
|
324
|
+
if (existing.apiKey) {
|
|
325
|
+
ctlRef.current?.setDefaultProvider(v);
|
|
326
|
+
ctlRef.current?.setSessionProvider(v);
|
|
327
|
+
enterMain();
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
setProviderStep({ id: 'key', preset: existing });
|
|
331
|
+
}
|
|
266
332
|
return;
|
|
267
|
-
if (p.apiKey) {
|
|
268
|
-
ctlRef.current?.setDefaultProvider(p.name);
|
|
269
|
-
ctlRef.current?.setSessionProvider(p.name);
|
|
270
|
-
enterMain();
|
|
271
333
|
}
|
|
272
|
-
|
|
273
|
-
|
|
334
|
+
// Must be a preset
|
|
335
|
+
const preset = PROVIDER_PRESETS.find((p) => p.name === v);
|
|
336
|
+
if (!preset)
|
|
337
|
+
return;
|
|
338
|
+
// Ollama: connectivity check with 2s timeout
|
|
339
|
+
if (preset.name.toLowerCase() === 'ollama') {
|
|
340
|
+
setSystemLines((ls) => [
|
|
341
|
+
...ls.slice(-5),
|
|
342
|
+
{ text: t('wiz.provider.ollama.checking', { url: preset.baseUrl }), level: 'info' },
|
|
343
|
+
]);
|
|
344
|
+
let models = [...preset.models];
|
|
345
|
+
let defaultModel = preset.defaultModel;
|
|
346
|
+
try {
|
|
347
|
+
const controller = new AbortController();
|
|
348
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
349
|
+
const resp = await fetch(preset.baseUrl + '/models', { signal: controller.signal });
|
|
350
|
+
clearTimeout(timeout);
|
|
351
|
+
if (resp.ok) {
|
|
352
|
+
const data = (await resp.json());
|
|
353
|
+
const detected = data?.data?.map((m) => m.id).filter(Boolean) ?? [];
|
|
354
|
+
if (detected.length > 0) {
|
|
355
|
+
models = detected;
|
|
356
|
+
defaultModel = detected[0];
|
|
357
|
+
setSystemLines((ls) => [
|
|
358
|
+
...ls.slice(-5),
|
|
359
|
+
{ text: t('wiz.provider.ollama.found', { n: detected.length }), level: 'ok' },
|
|
360
|
+
]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
setSystemLines((ls) => [
|
|
366
|
+
...ls.slice(-5),
|
|
367
|
+
{ text: t('wiz.provider.ollama.notFound', { url: preset.baseUrl }), level: 'warn' },
|
|
368
|
+
]);
|
|
369
|
+
}
|
|
370
|
+
const ollamaProvider = { ...preset, apiKey: 'ollama-local', models, defaultModel };
|
|
371
|
+
ctlRef.current?.saveProvider(ollamaProvider);
|
|
372
|
+
ctlRef.current?.setSessionProvider(ollamaProvider.name);
|
|
373
|
+
setPhase('model');
|
|
374
|
+
return;
|
|
274
375
|
}
|
|
376
|
+
setProviderStep({ id: 'key', preset: { ...preset, models: [...preset.models] } });
|
|
275
377
|
} }) })), phase === 'provider' && providerStep.id === 'key' && (_jsxs(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.preset.name }), footer: t('wiz.provider.key.footer'), children: [_jsx(Text, { color: "gray", children: providerStep.preset.baseUrl }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (k) => finishProvider({ ...providerStep.preset, 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: [], 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: [], allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onBack: wizardBack, onInput: (url) => setProviderStep({ id: 'model', name: providerStep.name, url }) }) })), phase === 'provider' && providerStep.id === 'model' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.model.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onInput: (model) => setProviderStep({ id: 'newKey', name: providerStep.name, url: providerStep.url, model }) }) })), phase === 'provider' && providerStep.id === 'newKey' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.name }), footer: t('wiz.provider.key.footer'), children: _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (key) => finishProvider({
|
|
276
378
|
name: providerStep.name,
|
|
277
379
|
baseUrl: providerStep.url,
|
|
@@ -301,7 +403,7 @@ export function App({ config, initialFolder }) {
|
|
|
301
403
|
const question = approval ? undefined : ctl.questions[0]; // approvals take priority
|
|
302
404
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
303
405
|
const inputActive = inputReady && !approval && !question && !settingsOpen;
|
|
304
|
-
return (_jsx(MainScreen, { ctl: ctl, folder: folder, view: view, focus: focus, systemLines: systemLines, agentNames: agentNames, approval: approval, question: question, inputActive: inputActive, onInput: (value, images) => {
|
|
406
|
+
return (_jsx(MainScreen, { ctl: ctl, folder: folder, view: view, focus: focus, rawLogs: rawLogs, systemLines: systemLines, agentNames: agentNames, approval: approval, question: question, inputActive: inputActive, onInput: (value, images) => {
|
|
305
407
|
const v = value.trim();
|
|
306
408
|
// Focus mode: plain text goes straight to the focused agent.
|
|
307
409
|
if (focus && v && !v.startsWith('/') && !v.startsWith('@')) {
|
|
@@ -315,86 +417,183 @@ export function App({ config, initialFolder }) {
|
|
|
315
417
|
setView('agents');
|
|
316
418
|
else if (focus) {
|
|
317
419
|
setFocus(null);
|
|
318
|
-
ui.system(t('m.focusOff'));
|
|
420
|
+
ui.system(t('m.focusOff'), 'info');
|
|
319
421
|
}
|
|
320
422
|
}, notify: ui.system }));
|
|
321
423
|
}
|
|
322
|
-
function MainScreen({ ctl, folder, view, focus, systemLines, agentNames, approval, question, inputActive, onInput, onEscape, notify, }) {
|
|
424
|
+
function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames, approval, question, inputActive, onInput, onEscape, notify, }) {
|
|
323
425
|
const agents = [...ctl.board.agents.values()];
|
|
324
426
|
// Adapt the layout to the REAL terminal size (never resize the user's terminal).
|
|
325
427
|
const { stdout } = useStdout();
|
|
326
428
|
const cols = stdout?.columns ?? 100;
|
|
327
|
-
const
|
|
328
|
-
const logsPerAgent = agents.length <= 1 ? 10 : agents.length <= 2 ? 7 : 5;
|
|
329
|
-
const width = agents.length === 1 || narrow ? '100%' : '50%';
|
|
429
|
+
const rows = stdout?.rows ?? 30;
|
|
330
430
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
431
|
+
// Height budget: fixed sections → body gets the remainder.
|
|
432
|
+
const headerLines = 4; // border-box header (top border + 2 content lines + bottom border)
|
|
433
|
+
const footerLine2 = 1; // always shown
|
|
434
|
+
const footerLine1 = agents.length === 0 ? 1 : 0;
|
|
435
|
+
const footerLines = footerLine1 + footerLine2;
|
|
436
|
+
// System messages: count actual rendered lines (including \n splits + "Session" label).
|
|
437
|
+
const systemMsgLines = systemLines.length > 0 && !settingsOpen
|
|
438
|
+
? (agents.length > 0 ? 1 : 0) + // "Session" label
|
|
439
|
+
(agents.length > 0
|
|
440
|
+
? systemLines
|
|
441
|
+
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
442
|
+
.slice(-2)
|
|
443
|
+
: systemLines).reduce((sum, l) => sum + l.text.split('\n').length, 0)
|
|
444
|
+
: 0;
|
|
445
|
+
const inputLines = 4; // modeHint (1) + input border box (3)
|
|
446
|
+
const spacerLines = 2; // after header + before footer
|
|
447
|
+
const approvalHeight = approval ? 6 : 0;
|
|
448
|
+
const questionHeight = question ? 7 : 0;
|
|
449
|
+
const bodyHeight = Math.max(1, rows - headerLines - footerLines - systemMsgLines - inputLines - spacerLines - approvalHeight - questionHeight);
|
|
331
450
|
// Focus mode: one agent rendered alone, with scrollback (PgUp/PgDn).
|
|
332
451
|
const focused = focus
|
|
333
452
|
? agents.find((a) => a.name.toLowerCase() === focus.toLowerCase())
|
|
334
453
|
: undefined;
|
|
335
454
|
const [scroll, setScroll] = useState(0);
|
|
455
|
+
const [focusFollowTail, setFocusFollowTail] = useState(true);
|
|
336
456
|
useEffect(() => setScroll(0), [focus]);
|
|
337
|
-
const FOCUS_LOGS =
|
|
457
|
+
const FOCUS_LOGS = Math.max(8, bodyHeight - 1);
|
|
338
458
|
const focusedLogs = focused ? ctl.board.logs.filter((l) => l.agentId === focused.id) : [];
|
|
339
459
|
const maxScroll = Math.max(0, focusedLogs.length - FOCUS_LOGS);
|
|
340
460
|
const clampedScroll = Math.min(scroll, maxScroll);
|
|
341
461
|
const visibleLogs = focused
|
|
342
462
|
? focusedLogs.slice(Math.max(0, focusedLogs.length - FOCUS_LOGS - clampedScroll), focusedLogs.length - clampedScroll)
|
|
343
463
|
: [];
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
464
|
+
const [hubScroll, setHubScroll] = useState(0);
|
|
465
|
+
const [hubFollowTail, setHubFollowTail] = useState(true);
|
|
466
|
+
const hubRows = Math.max(6, bodyHeight - 2);
|
|
467
|
+
const maxHubScroll = Math.max(0, agents.length - hubRows);
|
|
468
|
+
const clampedHub = Math.min(hubScroll, maxHubScroll);
|
|
469
|
+
const logSeq = ctl.board.logs.length > 0 ? ctl.board.logs[ctl.board.logs.length - 1].seq ?? ctl.board.logs.length : 0;
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
if (focusFollowTail)
|
|
472
|
+
setScroll(0);
|
|
473
|
+
}, [logSeq, focused?.state, focusFollowTail]);
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
if (hubFollowTail)
|
|
476
|
+
setHubScroll(0);
|
|
477
|
+
}, [logSeq, agents.length, hubFollowTail]);
|
|
478
|
+
// Scroll helpers (also used by mouse wheel handler below).
|
|
479
|
+
const scrollFocusUp = () => {
|
|
480
|
+
setFocusFollowTail(false);
|
|
481
|
+
setScroll((s) => Math.min(s + 1, maxScroll));
|
|
482
|
+
};
|
|
483
|
+
const scrollFocusDown = () => {
|
|
484
|
+
setScroll((s) => {
|
|
485
|
+
const next = Math.max(0, s - 1);
|
|
486
|
+
if (next === 0)
|
|
487
|
+
setFocusFollowTail(true);
|
|
488
|
+
return next;
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
const scrollHubUp = () => {
|
|
492
|
+
setHubFollowTail(false);
|
|
493
|
+
setHubScroll((s) => Math.min(Math.min(s, maxHubScroll) + 1, maxHubScroll));
|
|
494
|
+
};
|
|
495
|
+
const scrollHubDown = () => {
|
|
496
|
+
setHubScroll((s) => {
|
|
497
|
+
const next = Math.max(0, Math.min(s, maxHubScroll) - 1);
|
|
498
|
+
if (next === 0)
|
|
499
|
+
setHubFollowTail(true);
|
|
500
|
+
return next;
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
// Keyboard: Esc / PgUp-PgDn / Up-Down arrows.
|
|
504
|
+
// When CommandInput is NOT focused, Up/Down scroll the hub or focused agent;
|
|
505
|
+
// when it IS focused, CommandInput's own useInput sees them first (history nav).
|
|
359
506
|
useInput((_input, key) => {
|
|
360
507
|
if (key.escape)
|
|
361
508
|
onEscape();
|
|
362
509
|
if (focused) {
|
|
363
|
-
if (key.pageUp)
|
|
364
|
-
|
|
365
|
-
if (key.pageDown)
|
|
366
|
-
|
|
510
|
+
if (key.pageUp || key.upArrow)
|
|
511
|
+
scrollFocusUp();
|
|
512
|
+
if (key.pageDown || key.downArrow)
|
|
513
|
+
scrollFocusDown();
|
|
367
514
|
}
|
|
368
515
|
else if (view === 'agents') {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
516
|
+
if (key.pageUp || key.upArrow)
|
|
517
|
+
scrollHubUp();
|
|
518
|
+
if (key.pageDown || key.downArrow)
|
|
519
|
+
scrollHubDown();
|
|
520
|
+
}
|
|
521
|
+
}, { isActive: !inputActive });
|
|
522
|
+
const idleCount = agents.filter((a) => a.state === 'idle').length;
|
|
523
|
+
const workingCount = agents.filter((a) => ['working', 'thinking', 'listening'].includes(a.state)).length;
|
|
524
|
+
const doneCount = agents.filter((a) => a.state === 'done').length;
|
|
525
|
+
const errorCount = agents.filter((a) => ['error', 'stopped'].includes(a.state)).length;
|
|
526
|
+
const globalDotColor = workingCount > 0 ? 'green'
|
|
527
|
+
: agents.some((a) => ['waiting', 'paused'].includes(a.state)) ? 'yellow'
|
|
528
|
+
: 'gray';
|
|
529
|
+
const folderMax = Math.max(10, cols - 40);
|
|
530
|
+
// View breadcrumb: when not in agents view, show the view name instead of "control room".
|
|
531
|
+
const VIEW_LABEL = {
|
|
532
|
+
agents: 'control room',
|
|
533
|
+
board: 'coordination',
|
|
534
|
+
diff: 'diffs',
|
|
535
|
+
notes: 'notes',
|
|
536
|
+
help: 'help',
|
|
537
|
+
settings: 'settings',
|
|
538
|
+
'settings-session': 'session settings',
|
|
539
|
+
sessions: 'sessions',
|
|
540
|
+
cost: 'cost',
|
|
541
|
+
skills: 'skills',
|
|
542
|
+
specialists: 'specialists',
|
|
543
|
+
};
|
|
544
|
+
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
545
|
+
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", onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", 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, {})) : 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
|
|
546
|
+
? systemLines
|
|
547
|
+
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
548
|
+
.slice(-2)
|
|
549
|
+
: systemLines).flatMap((l, i) => {
|
|
550
|
+
const levelColor = l.level === 'ok' ? UI.ok :
|
|
551
|
+
l.level === 'warn' ? UI.warn :
|
|
552
|
+
l.level === 'error' ? UI.danger :
|
|
553
|
+
'gray';
|
|
554
|
+
// Split on \n so multiline i18n messages render correctly (Ink <Text> doesn't interpret \n).
|
|
555
|
+
const lines = l.text.split('\n');
|
|
556
|
+
return lines.map((line, j) => (_jsx(Text, { color: levelColor, wrap: "truncate-end", children: line }, `${i}-${j}`)));
|
|
557
|
+
})] })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(CommandInput, { active: inputActive, placeholder: focus ? `Message ${focus} or /command` : 'Task mode: describe work to run · /ask question · /plan proposal · / for commands', agentNames: agentNames, agents: agents, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", children: [agents.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: BRAND.muted, children: "/ask /task /plan" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Tab autocompletes \u00B7 Esc clears" })] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: CHROME.muted, children: "\u2318 Parallel" }), _jsx(Text, { color: CHROME.muted, children: " \u00B7 Shell " }), _jsx(Text, { color: ctl.session.approvalMode === 'ask' ? UI.warn :
|
|
558
|
+
ctl.session.approvalMode === 'yolo' ? UI.danger :
|
|
559
|
+
UI.ok, children: ctl.session.approvalMode === 'auto-safe' ? 'auto' : ctl.session.approvalMode }), _jsxs(Text, { color: CHROME.muted, children: [" \u00B7 Sessions: ", Controller.listSessions(ctl.projectRoot).length] }), ctl.questions.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u2753", ctl.questions.length] })) : null, ctl.approvals.length > 0 ? (_jsxs(Text, { color: UI.warn, children: [" \u00B7 \u23F3", ctl.approvals.length] })) : null, focused ? (_jsxs(Text, { color: BRAND.muted, children: [" \u00B7 \uD83C\uDFAF ", focused.name] })) : null] })] })] }));
|
|
560
|
+
}
|
|
561
|
+
function groupAgents(agents) {
|
|
562
|
+
const needs = agents.filter((a) => ['waiting', 'paused'].includes(a.state));
|
|
563
|
+
const working = agents.filter((a) => ['working', 'thinking', 'listening', 'idle'].includes(a.state));
|
|
564
|
+
const errors = agents.filter((a) => ['error', 'stopped'].includes(a.state));
|
|
565
|
+
const completed = agents.filter((a) => a.state === 'done');
|
|
566
|
+
return [
|
|
567
|
+
{ title: 'Needs input', color: UI.warn, agents: needs },
|
|
568
|
+
{ title: 'Working', color: UI.accent, agents: working },
|
|
569
|
+
{ title: 'Errors', color: UI.danger, agents: errors },
|
|
570
|
+
{ title: 'Completed', color: UI.ok, agents: completed },
|
|
571
|
+
].filter((g) => g.agents.length > 0);
|
|
572
|
+
}
|
|
573
|
+
function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
|
|
574
|
+
const groups = groupAgents([...agents].sort((a, b) => STATE_META[a.state].rank - STATE_META[b.state].rank || a.startedAt - b.startedAt));
|
|
575
|
+
let skipped = scroll;
|
|
576
|
+
let rendered = 0;
|
|
577
|
+
const rows = [];
|
|
578
|
+
for (const group of groups) {
|
|
579
|
+
const groupRows = [];
|
|
580
|
+
for (const agent of group.agents) {
|
|
581
|
+
if (skipped > 0) {
|
|
582
|
+
skipped--;
|
|
583
|
+
continue;
|
|
382
584
|
}
|
|
585
|
+
if (rendered >= visibleRows)
|
|
586
|
+
continue;
|
|
587
|
+
rendered++;
|
|
588
|
+
groupRows.push(_jsx(AgentRow, { agent: agent, logs: ctl.board.logsFor(agent.id, 8), cols: cols }, agent.id));
|
|
589
|
+
}
|
|
590
|
+
if (groupRows.length === 0)
|
|
591
|
+
continue;
|
|
592
|
+
if (rows.length > 0) {
|
|
593
|
+
rows.push(_jsx(Text, { color: CHROME.separator, children: '─'.repeat(cols - 2) }, `sep-${group.title}`));
|
|
383
594
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: "cyan", color: "black", bold: true, children: [' ', "\u2302 HUB", ' '] }), _jsxs(Text, { bold: true, color: "cyanBright", children: [' ', LOGO] })] }), _jsxs(Text, { color: "gray", children: [p ? `${p.name}:${ctl.session.model}` : '—', " \u00B7 ", p ? p.baseUrl.replace(/^https?:\/\//, '') : '', " \u00B7", ' ', ctl.session.approvalMode, " \u00B7 ", ctl.session.soundEnabled ? '🔔' : '🔕'] })] }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\uD83D\uDCC1 ", folder] }), view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", 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, {})) : agents.length === 0 ? (_jsx(Box, { borderStyle: "round", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentPanel, { agent: focused, logs: visibleLogs, width: "100%", expanded: true }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [t('m.focusHint', { name: focused.name }), clampedScroll > 0 ? ` · ↑${clampedScroll}` : ''] })] })) : (_jsxs(Box, { flexDirection: "column", children: [clampedGrid > 0 ? _jsx(Text, { color: "gray", children: t('grid.above', { n: clampedGrid }) }) : null, _jsx(Box, { flexWrap: "wrap", children: visibleAgents.map((a) => (_jsx(AgentPanel, { agent: a, logs: solo
|
|
389
|
-
? soloLogs.slice(Math.max(0, soloLogs.length - logsPerAgent - clampedSolo), soloLogs.length - clampedSolo)
|
|
390
|
-
: ctl.board.logsFor(a.id, logsPerAgent), width: width }, a.id))) }), solo && clampedSolo > 0 ? (_jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u2191", clampedSolo, " \u00B7 PgDn \u21E3"] })) : null, agents.length - clampedGrid - visibleAgents.length > 0 ? (_jsx(Text, { color: "gray", children: t('grid.below', { n: agents.length - clampedGrid - visibleAgents.length }) })) : null] })), systemLines.length > 0 && !settingsOpen && (_jsx(Box, { flexDirection: "column", children: systemLines.map((l, i) => (_jsx(Text, { color: "gray", wrap: "truncate-end", children: l }, i))) })), approval && (_jsx(ApprovalPrompt, { request: approval, pendingCount: ctl.approvals.length, onAnswer: (id, ok, always) => ctl.answerApproval(id, ok, always) })), question && (_jsx(QuestionPrompt, { question: question, pendingCount: ctl.questions.length, onAnswer: (id, answer, auto) => ctl.answerQuestion(id, answer, auto) }, question.id)), _jsx(CommandInput, { active: inputActive, placeholder: t('main.placeholder'), agentNames: agentNames, onSubmit: onInput, onEscape: onEscape, notify: notify }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: agents.length === 0
|
|
391
|
-
? t('main.status')
|
|
392
|
-
: t('status.bar', {
|
|
393
|
-
agents: agents.length,
|
|
394
|
-
active: activeCount,
|
|
395
|
-
cost: fmtCost(totalCost),
|
|
396
|
-
}) +
|
|
397
|
-
(ctl.questions.length > 0 ? ` · ❓${ctl.questions.length}` : '') +
|
|
398
|
-
(ctl.approvals.length > 0 ? ` · ⏳${ctl.approvals.length}` : '') +
|
|
399
|
-
(focused ? ` · 🎯 ${focused.name}` : '') })] }));
|
|
595
|
+
rows.push(_jsx(Box, { flexDirection: "column", children: groupRows }, group.title));
|
|
596
|
+
}
|
|
597
|
+
const below = Math.max(0, agents.length - scroll - rendered);
|
|
598
|
+
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] }));
|
|
400
599
|
}
|
package/dist/ui/AttachApp.js
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import net from 'node:net';
|
|
4
4
|
import { Box, Static, Text, useApp } from 'ink';
|
|
5
5
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
6
6
|
import { CommandInput } from './CommandInput.js';
|
|
7
|
-
import { KIND_COLOR, KIND_DIM } from './AgentPanel.js';
|
|
7
|
+
import { formatAgentTelemetry, KIND_COLOR, KIND_DIM } from './AgentPanel.js';
|
|
8
8
|
import { Md } from './Md.js';
|
|
9
9
|
import { QuestionPrompt } from './QuestionPrompt.js';
|
|
10
10
|
import { Spinner } from './Spinner.js';
|
|
11
|
-
import {
|
|
11
|
+
import { Timeline } from './Timeline.js';
|
|
12
|
+
import { stateLabel, elapsed, truncate } from './theme.js';
|
|
12
13
|
import { fmtCost } from '../pricing.js';
|
|
13
14
|
import { t } from '../i18n.js';
|
|
15
|
+
import { STATE_META, UI, middleTruncate } from './tokens.js';
|
|
14
16
|
const noop = () => { };
|
|
17
|
+
export function parseAttachCommand(text) {
|
|
18
|
+
const v = text.trim();
|
|
19
|
+
if (!v)
|
|
20
|
+
return null;
|
|
21
|
+
if (v === '/quit' || v === '/exit' || v === '/detach')
|
|
22
|
+
return { type: 'detach' };
|
|
23
|
+
if (v === '/raw')
|
|
24
|
+
return { type: 'raw' };
|
|
25
|
+
const m = v.match(/^\/(ask|a|task|t|plan|p)\s+(.+)$/s);
|
|
26
|
+
if (m) {
|
|
27
|
+
const mode = m[1] === 'ask' || m[1] === 'a' ? 'ask' : m[1] === 'plan' || m[1] === 'p' ? 'plan' : 'task';
|
|
28
|
+
return { type: 'spawn', text: m[2].trim(), mode };
|
|
29
|
+
}
|
|
30
|
+
return { type: 'input', text: v };
|
|
31
|
+
}
|
|
32
|
+
export function formatAttachFooter(info) {
|
|
33
|
+
if (!info)
|
|
34
|
+
return 'Waiting for agent · /quit';
|
|
35
|
+
return `${middleTruncate(info.model, 28)} · ${formatAgentTelemetry(info)} · plain text steers · /task new · /quit`;
|
|
36
|
+
}
|
|
15
37
|
export function AttachApp({ agentRef, sock }) {
|
|
16
38
|
const { exit } = useApp();
|
|
17
39
|
const [info, setInfo] = useState(null);
|
|
@@ -20,6 +42,7 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
20
42
|
const [approval, setApproval] = useState(null);
|
|
21
43
|
const [question, setQuestion] = useState(null);
|
|
22
44
|
const [gone, setGone] = useState(false);
|
|
45
|
+
const [raw, setRaw] = useState(false);
|
|
23
46
|
const socketRef = useRef(null);
|
|
24
47
|
const keySeq = useRef(0);
|
|
25
48
|
const lastBellId = useRef('');
|
|
@@ -83,44 +106,46 @@ export function AttachApp({ agentRef, sock }) {
|
|
|
83
106
|
socketRef.current?.write(JSON.stringify(msg) + '\n');
|
|
84
107
|
};
|
|
85
108
|
const send = (text) => {
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
109
|
+
const cmd = parseAttachCommand(text);
|
|
110
|
+
if (!cmd)
|
|
88
111
|
return;
|
|
89
|
-
if (
|
|
112
|
+
if (cmd.type === 'detach') {
|
|
90
113
|
exit();
|
|
91
114
|
return;
|
|
92
115
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
if (cmd.type === 'raw') {
|
|
117
|
+
setRaw((r) => !r);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// /task|/ask|/plan <text> — launch agent N+1 from this terminal.
|
|
121
|
+
if (cmd.type === 'spawn') {
|
|
122
|
+
wire({ type: 'spawn', text: cmd.text, mode: cmd.mode });
|
|
98
123
|
return;
|
|
99
124
|
}
|
|
100
|
-
wire({ type: 'input', agent: agentRef, text:
|
|
125
|
+
wire({ type: 'input', agent: agentRef, text: cmd.text });
|
|
101
126
|
};
|
|
102
|
-
const st = info ?
|
|
127
|
+
const st = info ? STATE_META[info.state] : null;
|
|
103
128
|
const busy = info ? ['thinking', 'working', 'listening'].includes(info.state) : false;
|
|
104
129
|
const interacting = Boolean(approval || question);
|
|
105
|
-
const banner = (_jsxs(Text, { wrap: "truncate-end", children: [
|
|
106
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: lines, children: (item) => (_jsx(Text, { color: KIND_COLOR[item.log.kind] ?? 'white', italic: KIND_DIM[item.log.kind] ?? false, wrap: "wrap", children: item.log.text }, item.key)) }), busy && info && st && !interacting ? (
|
|
130
|
+
const banner = (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: UI.brand, bold: true, children: t('attach.banner') }), info ? (_jsxs(Text, { color: info.color, bold: true, children: [' ', info.name, info.alias && info.alias !== info.name ? _jsxs(Text, { color: UI.muted, children: [" @", info.alias] }) : null] })) : null] }));
|
|
131
|
+
return (_jsxs(Box, { flexDirection: "column", children: [raw ? (_jsx(Static, { items: lines, children: (item) => (_jsx(Text, { color: KIND_COLOR[item.log.kind] ?? 'white', italic: KIND_DIM[item.log.kind] ?? false, wrap: "wrap", children: item.log.text }, item.key)) })) : null, busy && info && st && !interacting ? (
|
|
107
132
|
/* COMPACT region while the agent runs: small + borderless, so Ink's
|
|
108
133
|
* constant repaints (spinner ticks) never erase tall zones — this is
|
|
109
134
|
* what used to leave stray blank lines in the native scrollback. */
|
|
110
|
-
_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [
|
|
135
|
+
_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: info.color, bold: true, children: info.alias || info.name }), ' ', _jsxs(Text, { color: st.color, bold: true, children: [st.mark, " ", st.label] }), ' ', _jsx(Spinner, { color: info.color }), _jsxs(Text, { color: UI.muted, children: [' ', "\u00B7 ", elapsed(info.startedAt), " \u00B7 ", info.steps, " st \u00B7", ' ', Math.round((info.tokensIn + info.tokensOut) / 1000), "k \u00B7", ' '] }), _jsx(Text, { color: UI.ok, children: info.cost === null ? '$-' : fmtCost(info.cost) })] }), info.currentAction ? (_jsxs(Text, { color: info.color, wrap: "truncate-end", children: ["Current ", truncate(info.currentAction, 120)] })) : null, others.length > 0 ? (_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
111
136
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
112
|
-
.join(' · ')] })) : null] })) : (
|
|
137
|
+
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log) })] })) : null] })) : (
|
|
113
138
|
/* FULL panel when idle / waiting / done — repaints are rare here. */
|
|
114
|
-
_jsxs(Box, { borderStyle: "
|
|
139
|
+
_jsxs(Box, { borderStyle: "single", borderColor: info?.color ?? 'gray', flexDirection: "column", paddingX: 1, marginTop: 1, children: [info && st ? (_jsxs(_Fragment, { children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [banner, _jsxs(Text, { color: st.color, bold: true, children: [' ', st.mark, " ", st.label] })] }), _jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: [middleTruncate(info.model, 18), " \u00B7 ", elapsed(info.startedAt), " \u00B7 ", info.steps, " st \u00B7", ' ', Math.round((info.tokensIn + info.tokensOut) / 1000), "k \u00B7", ' ', info.ctxPct !== undefined ? (_jsxs(Text, { color: info.ctxPct >= 90 ? UI.danger : info.ctxPct >= 70 ? UI.warn : UI.muted, children: [info.ctxPct, "% \u00B7", ' '] })) : null, _jsx(Text, { color: UI.ok, children: info.cost === null ? '$-' : fmtCost(info.cost) })] })] }), _jsxs(Text, { color: UI.muted, wrap: "wrap", children: ["Task ", _jsx(Text, { color: UI.text, children: info.task })] }), info.currentAction ? (_jsxs(Text, { color: info.color, wrap: "truncate-end", children: ["Current ", truncate(info.currentAction, 160)] })) : null, others.length > 0 ? (
|
|
115
140
|
// The session's shared awareness, visible here too: what the
|
|
116
141
|
// OTHER agents are doing right now (live, same feed the agents get).
|
|
117
|
-
_jsxs(Text, { color:
|
|
142
|
+
_jsxs(Text, { color: UI.muted, wrap: "truncate-end", children: ["Others ", ' ', others
|
|
118
143
|
.map((o) => `${o.name} [${stateLabel(o.state)}] ${truncate(o.currentAction || o.task, 40)}`)
|
|
119
|
-
.join(' · ')] })) : null, info.lastResult && (info.state === 'done' || info.state === 'error' || info.state === 'stopped') ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color:
|
|
144
|
+
.join(' · ')] })) : null, !raw ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: UI.muted, bold: true, children: t('timeline.activity') }), _jsx(Timeline, { logs: lines.map((l) => l.log) })] })) : null, info.lastResult && (info.state === 'done' || info.state === 'error' || info.state === 'stopped') ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: UI.ok, bold: true, children: "Result" }), _jsx(Md, { text: info.lastResult })] })) : null] })) : (_jsx(Text, { color: "gray", children: gone ? t('attach.gone') : t('attach.waiting', { agent: agentRef }) })), gone && info ? _jsx(Text, { color: UI.danger, children: t('attach.gone') }) : null] })), approval ? (_jsx(ApprovalPrompt, { request: { ...approval, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, ok, always) => {
|
|
120
145
|
wire({ type: 'approve', id, approved: ok, always: !!always });
|
|
121
146
|
setApproval(null);
|
|
122
147
|
} })) : question ? (_jsx(QuestionPrompt, { question: { ...question, agentId: info?.id ?? '', resolve: noop }, pendingCount: 1, onAnswer: (id, answer) => {
|
|
123
148
|
wire({ type: 'answer', id, text: answer });
|
|
124
149
|
setQuestion(null);
|
|
125
|
-
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), onSubmit: send, onEscape: () => exit() }), _jsx(Text, { color: "
|
|
150
|
+
} }, question.id)) : null, _jsx(CommandInput, { active: !gone && !interacting, placeholder: t('attach.placeholder', { agent: info?.name ?? agentRef }), onSubmit: send, onEscape: () => exit() }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { color: "yellowBright", wrap: "truncate-end", children: formatAttachFooter(info) }) })] }));
|
|
126
151
|
}
|