tycono 0.3.14-beta.10 → 0.3.14-beta.11
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 +106 -206
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,84 @@ const PanelModeInner: React.FC<PanelModeProps> = ({
|
|
|
352
382
|
}, [focusedWaveId, companyRoot]);
|
|
353
383
|
|
|
354
384
|
const leftWidth = 28;
|
|
385
|
+
const termCols = process.stdout.columns || 120;
|
|
386
|
+
|
|
387
|
+
// === RADICAL FIX: render entire Panel as ONE pre-formatted Text ===
|
|
388
|
+
// yoga-layout OOMs on 245+ column terminals with nested Box layout.
|
|
389
|
+
// Solution: build the entire screen as a string, render as single <Text>.
|
|
390
|
+
|
|
391
|
+
const buildScreen = (): string => {
|
|
392
|
+
const rightWidth = Math.max(40, termCols - leftWidth - 3);
|
|
393
|
+
const lines: string[] = [];
|
|
394
|
+
|
|
395
|
+
// Header
|
|
396
|
+
const waveTitle = `W${focusedWaveIndex} ${focusedWave?.directive?.slice(0, leftWidth - 6) || '(idle)'}`;
|
|
397
|
+
const tabBar = ['Stream', 'Docs', 'Info'].map(t =>
|
|
398
|
+
t.toLowerCase() === rightTab ? `[${t}]` : ` ${t} `
|
|
399
|
+
).join(' ');
|
|
400
|
+
lines.push(`${waveTitle.padEnd(leftWidth)} \u2502 ${tabBar}`);
|
|
401
|
+
|
|
402
|
+
// Sessions count
|
|
403
|
+
if (waveSessionCount > 0) {
|
|
404
|
+
lines.push(`${(waveSessionCount + ' sessions').padEnd(leftWidth)} \u2502`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// OrgTree (left) + Stream (right) side by side
|
|
408
|
+
const ceoIcon = waveScopedStatuses['ceo'] === 'working' ? '\u25CF' : waveScopedStatuses['ceo'] === 'done' ? '\u2713' : '\u25CB';
|
|
409
|
+
const treeLines = [`\u2500\u2500 Org Tree \u2500\u2500`, `${ceoIcon} CEO`];
|
|
410
|
+
const flatEntries = flattenTreeForText(waveScopedTree);
|
|
411
|
+
treeLines.push(...flatEntries);
|
|
412
|
+
|
|
413
|
+
// Stream lines (right side)
|
|
414
|
+
const streamLines: string[] = [];
|
|
415
|
+
if (rightTab === 'stream') {
|
|
416
|
+
const maxEv = Math.min(termHeight - 8, 20);
|
|
417
|
+
const visible = (selectedRoleId ? events.filter(e => e.roleId === selectedRoleId) : events).slice(-maxEv);
|
|
418
|
+
for (const ev of visible) {
|
|
419
|
+
const line = eventToOneLiner(ev);
|
|
420
|
+
if (line) streamLines.push(line.slice(0, rightWidth));
|
|
421
|
+
}
|
|
422
|
+
if (streamLines.length === 0) {
|
|
423
|
+
streamLines.push(waveId ? 'Waiting for events...' : 'No active stream.');
|
|
424
|
+
}
|
|
425
|
+
} else if (rightTab === 'info') {
|
|
426
|
+
streamLines.push(`Wave: ${focusedWave?.waveId ?? 'none'}`);
|
|
427
|
+
streamLines.push(`Directive: ${focusedWave?.directive || '(idle)'}`);
|
|
428
|
+
streamLines.push(`Sessions: ${waveSessionCount}`);
|
|
429
|
+
streamLines.push(`SSE events: ${events.length}`);
|
|
430
|
+
} else {
|
|
431
|
+
streamLines.push('(Docs tab — press h/l to switch)');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Merge left + right
|
|
435
|
+
const maxLines = Math.max(treeLines.length, streamLines.length);
|
|
436
|
+
for (let i = 0; i < maxLines; i++) {
|
|
437
|
+
const left = (treeLines[i] ?? '').padEnd(leftWidth);
|
|
438
|
+
const right = streamLines[i] ?? '';
|
|
439
|
+
lines.push(`${left} \u2502 ${right}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Wave tabs
|
|
443
|
+
if (waves.length > 1) {
|
|
444
|
+
const waveTabs = waves.map((w, i) =>
|
|
445
|
+
w.waveId === focusedWaveId ? `[${i + 1}]` : ` ${i + 1} `
|
|
446
|
+
).join(' ');
|
|
447
|
+
lines.push('');
|
|
448
|
+
lines.push(waveTabs);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return lines.join('\n');
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Render entire panel as single Text to avoid yoga OOM
|
|
455
|
+
const screen = buildScreen();
|
|
355
456
|
|
|
356
457
|
return (
|
|
357
|
-
<Box flexDirection="column"
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
<Text color="green" bold>W{focusedWaveIndex}</Text>
|
|
363
|
-
{wavePreset && wavePreset !== 'default' && (
|
|
364
|
-
<Text color="magenta"> ({wavePreset})</Text>
|
|
365
|
-
)}
|
|
366
|
-
<Text color="gray"> </Text>
|
|
367
|
-
<Text color="white" wrap="truncate">
|
|
368
|
-
{focusedWave?.directive ? focusedWave.directive.slice(0, leftWidth - 6) : '(idle)'}
|
|
369
|
-
</Text>
|
|
370
|
-
</Box>
|
|
371
|
-
{waveSessionCount > 0 && (
|
|
372
|
-
<Box paddingX={1}>
|
|
373
|
-
<Text color="gray">{waveSessionCount} sessions</Text>
|
|
374
|
-
</Box>
|
|
375
|
-
)}
|
|
376
|
-
|
|
377
|
-
<OrgTree
|
|
378
|
-
tree={waveScopedTree}
|
|
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>
|
|
552
|
-
|
|
553
|
-
{/* Footer */}
|
|
554
|
-
<Box width="100%">
|
|
555
|
-
<Text color="gray">{'\u2500'.repeat(process.stdout.columns || 70)}</Text>
|
|
556
|
-
</Box>
|
|
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>
|
|
458
|
+
<Box flexDirection="column">
|
|
459
|
+
<Text wrap="truncate">{screen}</Text>
|
|
460
|
+
<Text color="gray" dimColor>
|
|
461
|
+
[h/l] tab [j/k] role {waves.length > 1 ? '[1-9] wave ' : ''}[Esc] command
|
|
462
|
+
</Text>
|
|
563
463
|
</Box>
|
|
564
464
|
);
|
|
565
465
|
};
|