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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.3.14-beta.10",
3
+ "version": "0.3.14-beta.11",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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" flexGrow={1}>
358
- <Box flexGrow={1}>
359
- {/* Left: Wave title + Org Tree + Wave tabs */}
360
- <Box flexDirection="column" width={leftWidth}>
361
- <Box paddingX={1}>
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
  };