tycono 0.3.14-beta.10 → 0.3.14-beta.12
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/package.json +1 -1
- package/src/tui/components/PanelMode.tsx +103 -204
package/package.json
CHANGED
|
@@ -45,6 +45,36 @@ interface PanelModeProps {
|
|
|
45
45
|
onFocusWave: (waveId: string) => void;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function flattenTreeForText(nodes: OrgNode[], isLast: boolean[] = []): string[] {
|
|
49
|
+
const lines: string[] = [];
|
|
50
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
51
|
+
const node = nodes[i];
|
|
52
|
+
const last = i === nodes.length - 1;
|
|
53
|
+
let prefix = '';
|
|
54
|
+
for (const l of isLast) prefix += l ? ' ' : '\u2502 ';
|
|
55
|
+
prefix += last ? '\u2514\u2500 ' : '\u251C\u2500 ';
|
|
56
|
+
const icon = node.status === 'working' ? '\u25CF' : node.status === 'done' ? '\u2713' : '\u25CB';
|
|
57
|
+
lines.push(`${prefix}${icon} ${node.role.id}`);
|
|
58
|
+
if (node.children.length > 0) lines.push(...flattenTreeForText(node.children, [...isLast, last]));
|
|
59
|
+
}
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function eventToOneLiner(event: SSEEvent): string | null {
|
|
64
|
+
const time = new Date(event.ts).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
65
|
+
const role = event.roleId.padEnd(12);
|
|
66
|
+
switch (event.type) {
|
|
67
|
+
case 'text': { const t = ((event.data.text as string) ?? '').trim(); return t ? `${time} ${role} ${t.slice(0, 100)}` : null; }
|
|
68
|
+
case 'thinking': { const t = ((event.data.text as string) ?? '').trim(); return t ? `${time} ${role} \uD83D\uDCAD ${t.slice(0, 80)}` : null; }
|
|
69
|
+
case 'tool:start': { const n = (event.data.name as string) ?? ''; return `${time} ${role} \u2192 ${n}`; }
|
|
70
|
+
case 'msg:start': return `${time} ${role} \u25B6 Started`;
|
|
71
|
+
case 'msg:done': return `${time} ${role} \u2713 Done`;
|
|
72
|
+
case 'msg:error': return `${time} ${role} \u2717 Error`;
|
|
73
|
+
case 'dispatch:start': return `${time} ${role} \u21D2 ${event.data.targetRole as string ?? ''}`;
|
|
74
|
+
default: return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
function getWaveScopedStatuses(
|
|
49
79
|
allSessions: SessionInfo[],
|
|
50
80
|
focusedWaveId: string | null,
|
|
@@ -352,214 +382,83 @@ const PanelModeInner: React.FC<PanelModeProps> = ({
|
|
|
352
382
|
}, [focusedWaveId, companyRoot]);
|
|
353
383
|
|
|
354
384
|
const leftWidth = 28;
|
|
385
|
+
const termCols = process.stdout.columns || 120;
|
|
386
|
+
const rightWidth = Math.max(40, termCols - leftWidth - 3);
|
|
387
|
+
|
|
388
|
+
// === Build panel as line arrays, render each line as <Text> ===
|
|
389
|
+
// yoga-layout OOMs with nested Box on 245+ columns.
|
|
390
|
+
// Solution: flat list of <Text> elements (1 yoga node per line, no nesting)
|
|
391
|
+
|
|
392
|
+
// Left: OrgTree
|
|
393
|
+
const ceoIcon = waveScopedStatuses['ceo'] === 'working' ? '\u25CF' : waveScopedStatuses['ceo'] === 'done' ? '\u2713' : '\u25CB';
|
|
394
|
+
const treeLines = [`${ceoIcon} CEO`, ...flattenTreeForText(waveScopedTree)];
|
|
395
|
+
|
|
396
|
+
// Right: Stream/Info content
|
|
397
|
+
const rightLines: string[] = [];
|
|
398
|
+
if (rightTab === 'stream') {
|
|
399
|
+
const maxEv = Math.min(termHeight - 8, 20);
|
|
400
|
+
const visible = (selectedRoleId ? events.filter(e => e.roleId === selectedRoleId) : events).slice(-maxEv);
|
|
401
|
+
for (const ev of visible) {
|
|
402
|
+
const line = eventToOneLiner(ev);
|
|
403
|
+
if (line) rightLines.push(line.slice(0, rightWidth));
|
|
404
|
+
}
|
|
405
|
+
if (rightLines.length === 0) rightLines.push(waveId ? 'Waiting for events...' : 'No active stream.');
|
|
406
|
+
} else if (rightTab === 'info') {
|
|
407
|
+
rightLines.push(`Wave: ${focusedWave?.waveId ?? 'none'}`);
|
|
408
|
+
if (wavePreset) rightLines.push(`Preset: ${wavePreset}`);
|
|
409
|
+
rightLines.push(`Directive: ${focusedWave?.directive?.slice(0, 60) || '(idle)'}`);
|
|
410
|
+
rightLines.push(`Sessions: ${waveSessionCount} Events: ${events.length}`);
|
|
411
|
+
} else {
|
|
412
|
+
rightLines.push('Docs tab (h/l to switch)');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Merge into display lines
|
|
416
|
+
const maxRows = Math.max(treeLines.length, rightLines.length);
|
|
417
|
+
const mergedLines: Array<{ left: string; right: string }> = [];
|
|
418
|
+
for (let i = 0; i < maxRows; i++) {
|
|
419
|
+
mergedLines.push({
|
|
420
|
+
left: (treeLines[i] ?? '').padEnd(leftWidth).slice(0, leftWidth),
|
|
421
|
+
right: (rightLines[i] ?? ''),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Tab bar
|
|
426
|
+
const tabLabels = ['Stream', 'Docs', 'Info'];
|
|
427
|
+
const tabBar = tabLabels.map(t => t.toLowerCase() === rightTab ? `[${t}]` : ` ${t} `).join(' ');
|
|
428
|
+
|
|
429
|
+
// Wave tabs
|
|
430
|
+
const waveTabs = waves.length > 1
|
|
431
|
+
? waves.map((w, i) => w.waveId === focusedWaveId ? `[${i + 1}]` : ` ${i + 1} `).join(' ')
|
|
432
|
+
: '';
|
|
355
433
|
|
|
356
434
|
return (
|
|
357
|
-
<Box flexDirection="column"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
focused={rightTab === 'stream'}
|
|
380
|
-
selectedIndex={selectedRoleIndex}
|
|
381
|
-
flatRoles={flatRoles}
|
|
382
|
-
ceoStatus={waveScopedStatuses['ceo'] ?? 'idle'}
|
|
383
|
-
/>
|
|
384
|
-
|
|
385
|
-
{waves.length > 1 && (
|
|
386
|
-
<Box paddingX={1} marginTop={1}>
|
|
387
|
-
{waves.map((w, i) => (
|
|
388
|
-
<Box key={w.waveId} marginRight={1}>
|
|
389
|
-
<Text
|
|
390
|
-
color={w.waveId === focusedWaveId ? 'green' : 'gray'}
|
|
391
|
-
bold={w.waveId === focusedWaveId}
|
|
392
|
-
inverse={w.waveId === focusedWaveId}
|
|
393
|
-
>{` ${i + 1} `}</Text>
|
|
394
|
-
</Box>
|
|
395
|
-
))}
|
|
396
|
-
</Box>
|
|
397
|
-
)}
|
|
398
|
-
</Box>
|
|
399
|
-
|
|
400
|
-
{/* Vertical separator — single character, not repeated newlines */}
|
|
401
|
-
<Text color="gray">{separatorStr}</Text>
|
|
402
|
-
|
|
403
|
-
{/* Right: Tabbed panel */}
|
|
404
|
-
<Box flexGrow={1} flexDirection="column">
|
|
405
|
-
{/* Tab bar */}
|
|
406
|
-
<Box paddingX={1} marginBottom={0}>
|
|
407
|
-
{(['stream', 'docs', 'info'] as RightTab[]).map(tab => (
|
|
408
|
-
<Box key={tab} marginRight={1}>
|
|
409
|
-
<Text
|
|
410
|
-
color={rightTab === tab ? 'cyan' : 'gray'}
|
|
411
|
-
bold={rightTab === tab}
|
|
412
|
-
inverse={rightTab === tab}
|
|
413
|
-
>
|
|
414
|
-
{` ${tab.charAt(0).toUpperCase() + tab.slice(1)} `}
|
|
415
|
-
</Text>
|
|
416
|
-
</Box>
|
|
417
|
-
))}
|
|
418
|
-
<Text color="gray" dimColor> [h/l] switch</Text>
|
|
419
|
-
</Box>
|
|
420
|
-
|
|
421
|
-
{/* Stream tab */}
|
|
422
|
-
{rightTab === 'stream' && (
|
|
423
|
-
<>
|
|
424
|
-
{selectedRoleId && selectedSession && (
|
|
425
|
-
<Box flexDirection="column" paddingX={1}>
|
|
426
|
-
<Box justifyContent="space-between">
|
|
427
|
-
<Text bold color="cyan">{selectedRoleId}</Text>
|
|
428
|
-
<Text color={selectedSession.status === 'active' ? 'green' : 'gray'}>
|
|
429
|
-
{selectedSession.status === 'active' ? '\u25CF' : '\u25CB'} {selectedSession.status}
|
|
430
|
-
{selectedSession.startedAt ? ` (${elapsed(selectedSession.startedAt)})` : ''}
|
|
431
|
-
</Text>
|
|
432
|
-
</Box>
|
|
433
|
-
{selectedSession.ports.api > 0 && (
|
|
434
|
-
<Text color="gray">Port API:{selectedSession.ports.api} Vite:{selectedSession.ports.vite}</Text>
|
|
435
|
-
)}
|
|
436
|
-
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
437
|
-
</Box>
|
|
438
|
-
)}
|
|
439
|
-
{selectedRoleId && !selectedSession && (
|
|
440
|
-
<Box flexDirection="column" paddingX={1}>
|
|
441
|
-
<Text bold color="cyan">{selectedRoleId}</Text>
|
|
442
|
-
<Text color="gray">(not active in this wave)</Text>
|
|
443
|
-
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
444
|
-
</Box>
|
|
445
|
-
)}
|
|
446
|
-
<StreamView
|
|
447
|
-
events={roleEvents}
|
|
448
|
-
allRoleIds={flatRoles}
|
|
449
|
-
streamStatus={streamStatus}
|
|
450
|
-
waveId={waveId}
|
|
451
|
-
roleLabel={roleLabel}
|
|
452
|
-
/>
|
|
453
|
-
</>
|
|
454
|
-
)}
|
|
455
|
-
|
|
456
|
-
{/* Docs tab — KB browser + wave artifacts */}
|
|
457
|
-
{rightTab === 'docs' && (
|
|
458
|
-
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
459
|
-
{/* Filter bar */}
|
|
460
|
-
<Box marginBottom={0}>
|
|
461
|
-
{(['all', 'wave', 'kb', 'projects'] as DocsFilter[]).map((f, i) => (
|
|
462
|
-
<Box key={f} marginRight={1}>
|
|
463
|
-
<Text
|
|
464
|
-
color={docsFilter === f ? 'cyan' : 'gray'}
|
|
465
|
-
bold={docsFilter === f}
|
|
466
|
-
inverse={docsFilter === f}
|
|
467
|
-
>
|
|
468
|
-
{f === 'wave' ? ` ${i + 1}:\u2605Wave ` : ` ${i + 1}:${f.charAt(0).toUpperCase() + f.slice(1)} `}
|
|
469
|
-
</Text>
|
|
470
|
-
</Box>
|
|
471
|
-
))}
|
|
472
|
-
<Text color="gray" dimColor> ({docsList.length})</Text>
|
|
473
|
-
</Box>
|
|
474
|
-
|
|
475
|
-
{docsList.length === 0 ? (
|
|
476
|
-
<Box marginTop={1}>
|
|
477
|
-
<Text color="gray">{docsFilter === 'wave' ? 'No files created in this wave.' : 'No documents found.'}</Text>
|
|
478
|
-
</Box>
|
|
479
|
-
) : (
|
|
480
|
-
<Box flexGrow={1} flexDirection="column">
|
|
481
|
-
{docsScroll === 0 ? (
|
|
482
|
-
/* File list — only render visible window (prevent Yoga OOM on 600+ files) */
|
|
483
|
-
<Box flexDirection="column" marginTop={0}>
|
|
484
|
-
<Text color="gray" dimColor>{docsList.length} files{docsIndex > 0 ? ` (${docsIndex + 1}/${docsList.length})` : ''}</Text>
|
|
485
|
-
{docsList.slice(docsIndex, docsIndex + termHeight - 10).map((doc, i) => (
|
|
486
|
-
<Box key={doc.path}>
|
|
487
|
-
<Text
|
|
488
|
-
color={i === 0 ? 'cyan' : doc.isWave ? 'green' : 'white'}
|
|
489
|
-
bold={i === 0}
|
|
490
|
-
inverse={i === 0}
|
|
491
|
-
>
|
|
492
|
-
{doc.isWave ? '\u2605' : ' '} {doc.title.slice(0, 55)}
|
|
493
|
-
</Text>
|
|
494
|
-
</Box>
|
|
495
|
-
))}
|
|
496
|
-
</Box>
|
|
497
|
-
) : (
|
|
498
|
-
/* File preview */
|
|
499
|
-
<Box flexDirection="column">
|
|
500
|
-
<Text color="cyan" bold>{selectedDoc?.isWave ? '\u2605 ' : ''}{selectedDoc?.path.split('/').slice(-2).join('/')}</Text>
|
|
501
|
-
<Text color="gray">{'\u2500'.repeat(50)}</Text>
|
|
502
|
-
{filePreview.slice(docsScroll - 1, docsScroll - 1 + termHeight - 10).map((line, i) => (
|
|
503
|
-
<Text key={i} color="white" wrap="wrap">{line}</Text>
|
|
504
|
-
))}
|
|
505
|
-
</Box>
|
|
506
|
-
)}
|
|
507
|
-
|
|
508
|
-
<Box marginTop={0}>
|
|
509
|
-
<Text color="gray" dimColor>
|
|
510
|
-
[Enter] {process.env.EDITOR || 'vim'} | [j/k] {docsScroll > 0 ? 'scroll' : 'select'}
|
|
511
|
-
</Text>
|
|
512
|
-
</Box>
|
|
513
|
-
</Box>
|
|
514
|
-
)}
|
|
515
|
-
</Box>
|
|
516
|
-
)}
|
|
517
|
-
|
|
518
|
-
{/* Info tab */}
|
|
519
|
-
{rightTab === 'info' && (
|
|
520
|
-
<Box flexDirection="column" paddingX={1}>
|
|
521
|
-
<Text bold color="cyan">Wave Info</Text>
|
|
522
|
-
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
523
|
-
<Text color="white">Wave: {focusedWave?.waveId ?? 'none'}</Text>
|
|
524
|
-
{wavePreset && <Text color="magenta">Preset: {wavePreset}</Text>}
|
|
525
|
-
<Text color="white">Directive: {focusedWave?.directive || '(idle)'}</Text>
|
|
526
|
-
<Text color="white">Sessions: {waveSessionCount}</Text>
|
|
527
|
-
<Text color="white">Files modified: {waveFileSet.size}</Text>
|
|
528
|
-
<Text color="white">SSE events: {events.length}</Text>
|
|
529
|
-
|
|
530
|
-
{/* Active sessions in this wave */}
|
|
531
|
-
{waveSessionCount > 0 && (
|
|
532
|
-
<>
|
|
533
|
-
<Text color="gray" bold>{'\n'}Active in this wave:</Text>
|
|
534
|
-
{allSessions
|
|
535
|
-
.filter(s => s.waveId === focusedWaveId && s.status === 'active')
|
|
536
|
-
.slice(0, 10)
|
|
537
|
-
.map(s => {
|
|
538
|
-
const port = activeSessions.find(a => a.sessionId === s.id);
|
|
539
|
-
return (
|
|
540
|
-
<Text key={s.id} color="white">
|
|
541
|
-
{` ${s.roleId.padEnd(12)} ${s.id.slice(0, 20)} ${port ? `API:${port.ports.api}` : ''}`}
|
|
542
|
-
</Text>
|
|
543
|
-
);
|
|
544
|
-
})
|
|
545
|
-
}
|
|
546
|
-
</>
|
|
547
|
-
)}
|
|
548
|
-
</Box>
|
|
549
|
-
)}
|
|
550
|
-
</Box>
|
|
551
|
-
</Box>
|
|
435
|
+
<Box flexDirection="column">
|
|
436
|
+
{/* Header */}
|
|
437
|
+
<Text>
|
|
438
|
+
<Text color="green" bold>W{focusedWaveIndex}</Text>
|
|
439
|
+
<Text color="white"> {focusedWave?.directive?.slice(0, leftWidth - 6) || '(idle)'}</Text>
|
|
440
|
+
<Text color="gray"> \u2502 </Text>
|
|
441
|
+
<Text color="cyan" bold>{tabBar}</Text>
|
|
442
|
+
</Text>
|
|
443
|
+
{waveSessionCount > 0 && <Text color="gray">{waveSessionCount} sessions</Text>}
|
|
444
|
+
<Text color="gray">\u2500\u2500 Org Tree \u2500\u2500</Text>
|
|
445
|
+
|
|
446
|
+
{/* Merged left|right lines — each line = 1 Text = 1 yoga node */}
|
|
447
|
+
{mergedLines.map((line, i) => (
|
|
448
|
+
<Text key={i}>
|
|
449
|
+
<Text color="white">{line.left}</Text>
|
|
450
|
+
<Text color="gray"> \u2502 </Text>
|
|
451
|
+
<Text color="white">{line.right}</Text>
|
|
452
|
+
</Text>
|
|
453
|
+
))}
|
|
454
|
+
|
|
455
|
+
{/* Wave tabs */}
|
|
456
|
+
{waveTabs && <Text color="gray">{waveTabs}</Text>}
|
|
552
457
|
|
|
553
458
|
{/* Footer */}
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
</
|
|
557
|
-
<Box paddingX={1} justifyContent="center">
|
|
558
|
-
<Text color="gray" dimColor>
|
|
559
|
-
[h/l] tab [j/k] {rightTab === 'stream' ? 'role' : 'scroll'} {rightTab === 'docs' ? '[Enter] vim ' : ''}
|
|
560
|
-
{waves.length > 1 ? '[1-9] wave ' : ''}[Esc] command
|
|
561
|
-
</Text>
|
|
562
|
-
</Box>
|
|
459
|
+
<Text color="gray" dimColor>
|
|
460
|
+
[h/l] tab [j/k] role {waves.length > 1 ? '[1-9] wave ' : ''}[Esc] command
|
|
461
|
+
</Text>
|
|
563
462
|
</Box>
|
|
564
463
|
);
|
|
565
464
|
};
|