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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.3.14-beta.10",
3
+ "version": "0.3.14-beta.12",
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,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" 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>
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
- <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>
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
  };