tycono 0.1.96-beta.30 → 0.1.96-beta.31

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.1.96-beta.30",
3
+ "version": "0.1.96-beta.31",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/app.tsx CHANGED
@@ -404,17 +404,14 @@ export const App: React.FC = () => {
404
404
  addSystemMessage(' /new [text] Create new wave', 'white');
405
405
  addSystemMessage(' /waves List all waves', 'white');
406
406
  addSystemMessage(' /focus <n> Switch to wave n', 'white');
407
- addSystemMessage(' /agents Agent tree + resources', 'white');
408
- addSystemMessage(' /ports Port allocations', 'white');
409
- addSystemMessage(' /sessions All sessions (kill/cleanup)', 'white');
407
+ addSystemMessage(' /agents Wave \u2192 Role \u2192 Session tree', 'white');
408
+ addSystemMessage(' /sessions Sessions + ports (kill/cleanup)', 'white');
410
409
  addSystemMessage(' /kill <id> Kill a session', 'white');
411
410
  addSystemMessage(' /cleanup Remove dead sessions', 'white');
412
- addSystemMessage(' /status Show current status', 'white');
413
- addSystemMessage(' /assign <role> <task> Assign task to role', 'white');
414
- addSystemMessage(' /roles Org tree (Panel Mode)', 'white');
415
- addSystemMessage(' /help Show this help', 'white');
416
- addSystemMessage(' /quit Exit TUI', 'white');
417
- addSystemMessage('Keys: [Tab] panel [Esc] back [Ctrl+C] quit', 'gray');
411
+ addSystemMessage(' /status Current status', 'white');
412
+ addSystemMessage(' /help This help', 'white');
413
+ addSystemMessage(' /quit Exit', 'white');
414
+ addSystemMessage('Keys: [Tab] team panel [1-9] wave [Esc] back [Ctrl+C] quit', 'gray');
418
415
  break;
419
416
  case 'info':
420
417
  if (result.message === '__status__') {
@@ -493,9 +490,9 @@ export const App: React.FC = () => {
493
490
  streamStatus={sse.streamStatus}
494
491
  waveId={focusedWaveId}
495
492
  activeSessions={api.activeSessions}
493
+ allSessions={api.sessions}
496
494
  waves={waves}
497
495
  focusedWaveId={focusedWaveId}
498
- portSummary={api.portSummary}
499
496
  onMove={(dir) => {
500
497
  const nextIdx = dir === 'up'
501
498
  ? Math.max(0, selectedRoleIndex - 1)
@@ -508,6 +505,10 @@ export const App: React.FC = () => {
508
505
  setSelectedRoleId(roleId === selectedRoleId ? null : roleId);
509
506
  }}
510
507
  onEscape={() => setMode('command')}
508
+ onFocusWave={(newWaveId) => {
509
+ setFocusedWaveId(newWaveId);
510
+ sse.clearEvents();
511
+ }}
511
512
  />
512
513
  </Box>
513
514
  <StatusBar
@@ -1,15 +1,16 @@
1
1
  /**
2
- * PanelMode — Tab view: Org Tree (left) + Agent Detail + Stream (right)
2
+ * PanelMode — Wave-scoped team view
3
3
  *
4
- * Left: Org Tree with status icons
5
- * + compact resource summary (waves, ports)
6
- * Right: Selected role's resource info (port, worktree, browser)
7
- * + event stream
4
+ * Shows focused wave's team state:
5
+ * Left: Wave title + Org Tree (wave-scoped status) + Wave tabs
6
+ * Right: Selected role's resources + stream
8
7
  *
9
8
  * Navigation:
10
- * j/k or arrow keys — move in Org Tree
11
- * Enter select role to view its stream
12
- * Esc return to Command Mode
9
+ * j/k — move in Org Tree (auto-selects)
10
+ * 1-9 switch wave focus
11
+ * Enter toggle filtered/all stream
12
+ * Esc — return to Command Mode
13
+ * Ctrl+C — quit
13
14
  */
14
15
 
15
16
  import React, { useState, useEffect, useMemo } from 'react';
@@ -17,7 +18,7 @@ import { Box, Text, useInput } from 'ink';
17
18
  import { OrgTree } from './OrgTree';
18
19
  import { StreamView } from './StreamView';
19
20
  import type { OrgNode } from '../store';
20
- import type { SSEEvent, ActiveSessionInfo } from '../api';
21
+ import type { SSEEvent, ActiveSessionInfo, SessionInfo } from '../api';
21
22
  import type { WaveInfo } from '../hooks/useCommand';
22
23
 
23
24
  interface PanelModeProps {
@@ -29,23 +30,51 @@ interface PanelModeProps {
29
30
  streamStatus: 'idle' | 'streaming' | 'done' | 'error';
30
31
  waveId: string | null;
31
32
  activeSessions: ActiveSessionInfo[];
33
+ allSessions: SessionInfo[];
32
34
  waves: WaveInfo[];
33
35
  focusedWaveId: string | null;
34
- portSummary: { active: number; totalPorts: number };
35
36
  onMove: (direction: 'up' | 'down') => void;
36
37
  onSelect: () => void;
37
38
  onEscape: () => void;
39
+ onFocusWave: (waveId: string) => void;
38
40
  }
39
41
 
40
- /** Find active session for a given roleId */
41
- function findSessionForRole(activeSessions: ActiveSessionInfo[], roleId: string): ActiveSessionInfo | null {
42
- // Prefer active sessions, then any
43
- return activeSessions.find(s => s.roleId === roleId && s.status === 'active')
44
- ?? activeSessions.find(s => s.roleId === roleId)
45
- ?? null;
42
+ /** Get wave-scoped role statuses */
43
+ function getWaveScopedStatuses(
44
+ allSessions: SessionInfo[],
45
+ focusedWaveId: string | null,
46
+ ): Record<string, string> {
47
+ if (!focusedWaveId) return {};
48
+ const statuses: Record<string, string> = {};
49
+ for (const s of allSessions) {
50
+ if (s.waveId !== focusedWaveId) continue;
51
+ if (s.status === 'active') {
52
+ statuses[s.roleId] = 'working';
53
+ } else if (!statuses[s.roleId]) {
54
+ statuses[s.roleId] = 'done';
55
+ }
56
+ }
57
+ return statuses;
58
+ }
59
+
60
+ /** Find active session for a role in focused wave */
61
+ function findSessionForRole(
62
+ activeSessions: ActiveSessionInfo[],
63
+ allSessions: SessionInfo[],
64
+ roleId: string,
65
+ focusedWaveId: string | null,
66
+ ): ActiveSessionInfo | null {
67
+ // First try: session with matching waveId
68
+ if (focusedWaveId) {
69
+ const waveSes = allSessions.find(s => s.waveId === focusedWaveId && s.roleId === roleId && s.status === 'active');
70
+ if (waveSes) {
71
+ return activeSessions.find(s => s.sessionId === waveSes.id) ?? null;
72
+ }
73
+ }
74
+ // Fallback: any active session for this role
75
+ return activeSessions.find(s => s.roleId === roleId && s.status === 'active') ?? null;
46
76
  }
47
77
 
48
- /** Format elapsed time */
49
78
  function elapsed(startedAt: string): string {
50
79
  const ms = Date.now() - new Date(startedAt).getTime();
51
80
  if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
@@ -62,14 +91,14 @@ export const PanelMode: React.FC<PanelModeProps> = ({
62
91
  streamStatus,
63
92
  waveId,
64
93
  activeSessions,
94
+ allSessions,
65
95
  waves,
66
96
  focusedWaveId,
67
- portSummary,
68
97
  onMove,
69
98
  onSelect,
70
99
  onEscape,
100
+ onFocusWave,
71
101
  }) => {
72
- // Track terminal height for vertical separator
73
102
  const [termHeight, setTermHeight] = useState(process.stdout.rows || 30);
74
103
  useEffect(() => {
75
104
  const onResize = () => setTermHeight(process.stdout.rows || 30);
@@ -77,25 +106,35 @@ export const PanelMode: React.FC<PanelModeProps> = ({
77
106
  return () => { process.stdout.off('resize', onResize); };
78
107
  }, []);
79
108
 
80
- // Memoize expensive strings
81
- const separatorStr = useMemo(() => '\u2502\n'.repeat(Math.max(5, termHeight - 6)), [termHeight]);
109
+ const separatorStr = useMemo(() => '\u2502\n'.repeat(Math.max(5, termHeight - 8)), [termHeight]);
82
110
 
83
- useInput((input, key) => {
84
- if (key.escape) {
85
- onEscape();
86
- return;
87
- }
88
- if (key.upArrow || input === 'k') {
89
- onMove('up');
90
- return;
91
- }
92
- if (key.downArrow || input === 'j') {
93
- onMove('down');
94
- return;
111
+ // Wave-scoped statuses for Org Tree
112
+ const waveScopedStatuses = useMemo(
113
+ () => getWaveScopedStatuses(allSessions, focusedWaveId),
114
+ [allSessions, focusedWaveId],
115
+ );
116
+
117
+ // Override tree node statuses with wave-scoped values
118
+ const waveScopedTree = useMemo(() => {
119
+ function scopeNode(node: OrgNode): OrgNode {
120
+ return {
121
+ ...node,
122
+ status: waveScopedStatuses[node.role.id] ?? 'idle',
123
+ children: node.children.map(scopeNode),
124
+ };
95
125
  }
96
- if (key.return) {
97
- onSelect();
98
- return;
126
+ return tree.map(scopeNode);
127
+ }, [tree, waveScopedStatuses]);
128
+
129
+ useInput((input, key) => {
130
+ if (key.escape) { onEscape(); return; }
131
+ if (key.upArrow || input === 'k') { onMove('up'); return; }
132
+ if (key.downArrow || input === 'j') { onMove('down'); return; }
133
+ if (key.return) { onSelect(); return; }
134
+ // 1-9: wave switch
135
+ const num = parseInt(input, 10);
136
+ if (num >= 1 && num <= 9 && num <= waves.length) {
137
+ onFocusWave(waves[num - 1].waveId);
99
138
  }
100
139
  });
101
140
 
@@ -108,9 +147,9 @@ export const PanelMode: React.FC<PanelModeProps> = ({
108
147
  ? flatRoles.includes(selectedRoleId) ? selectedRoleId : 'All'
109
148
  : 'All';
110
149
 
111
- // Find resource info for selected role
150
+ // Find resource info for selected role (wave-scoped)
112
151
  const selectedSession = selectedRoleId
113
- ? findSessionForRole(activeSessions, selectedRoleId)
152
+ ? findSessionForRole(activeSessions, allSessions, selectedRoleId, focusedWaveId)
114
153
  : null;
115
154
 
116
155
  // Focused wave info
@@ -119,29 +158,75 @@ export const PanelMode: React.FC<PanelModeProps> = ({
119
158
  ? waves.findIndex(w => w.waveId === focusedWaveId) + 1
120
159
  : 0;
121
160
 
161
+ // Wave session count for display
162
+ const waveSessionCount = focusedWaveId
163
+ ? allSessions.filter(s => s.waveId === focusedWaveId).length
164
+ : 0;
165
+
166
+ const leftWidth = 28;
167
+
122
168
  return (
123
169
  <Box flexDirection="column" flexGrow={1}>
124
- {/* Main content: Org Tree left | Detail + Stream right */}
170
+ {/* Main content */}
125
171
  <Box flexGrow={1}>
126
- {/* Left: Org Tree + Resource Summary */}
127
- <Box flexDirection="column" width={28}>
172
+ {/* Left: Wave title + Org Tree + Wave tabs */}
173
+ <Box flexDirection="column" width={leftWidth}>
174
+ {/* Wave title */}
175
+ <Box paddingX={1} marginBottom={0}>
176
+ <Text color="green" bold>
177
+ W{focusedWaveIndex}
178
+ </Text>
179
+ <Text color="gray"> </Text>
180
+ <Text color="white" wrap="truncate">
181
+ {focusedWave?.directive ? focusedWave.directive.slice(0, leftWidth - 6) : '(idle)'}
182
+ </Text>
183
+ </Box>
184
+
185
+ {/* Session count */}
186
+ {waveSessionCount > 0 && (
187
+ <Box paddingX={1}>
188
+ <Text color="gray">{waveSessionCount} sessions</Text>
189
+ </Box>
190
+ )}
191
+
192
+ {/* Org Tree (wave-scoped statuses) */}
128
193
  <OrgTree
129
- tree={tree}
194
+ tree={waveScopedTree}
130
195
  focused={true}
131
196
  selectedIndex={selectedRoleIndex}
132
197
  flatRoles={flatRoles}
133
- ceoStatus={activeSessions.some(s => s.roleId === 'ceo' && s.status === 'active') ? 'working' : 'idle'}
198
+ ceoStatus={waveScopedStatuses['ceo'] ?? 'idle'}
134
199
  />
200
+
201
+ {/* Wave tabs at bottom */}
202
+ {waves.length > 1 && (
203
+ <Box paddingX={1} marginTop={1}>
204
+ {waves.map((w, i) => {
205
+ const isFocused = w.waveId === focusedWaveId;
206
+ return (
207
+ <Box key={w.waveId} marginRight={1}>
208
+ <Text
209
+ color={isFocused ? 'green' : 'gray'}
210
+ bold={isFocused}
211
+ inverse={isFocused}
212
+ >
213
+ {` ${i + 1} `}
214
+ </Text>
215
+ </Box>
216
+ );
217
+ })}
218
+ </Box>
219
+ )}
135
220
  </Box>
136
221
 
137
- {/* Vertical separator — memoized to avoid regenerating on every render */}
222
+ {/* Vertical separator */}
138
223
  <Box flexDirection="column" marginX={0}>
139
224
  <Text color="gray">{separatorStr}</Text>
140
225
  </Box>
141
226
 
142
227
  {/* Right: Agent Detail + Stream */}
143
228
  <Box flexGrow={1} flexDirection="column" overflow="hidden">
144
- {/* Agent Resource Header — shown when a role is selected */}
229
+ {/* Agent Resource Header */}
145
230
  {selectedRoleId && selectedSession && (
146
231
  <Box flexDirection="column" paddingX={1} marginBottom={0}>
147
232
  <Box justifyContent="space-between">
@@ -151,54 +236,35 @@ export const PanelMode: React.FC<PanelModeProps> = ({
151
236
  {selectedSession.startedAt ? ` (${elapsed(selectedSession.startedAt)})` : ''}
152
237
  </Text>
153
238
  </Box>
154
-
155
- {/* Ports */}
156
- <Box>
157
- <Text color="gray">Port </Text>
158
- <Text color="white">
159
- API:{selectedSession.ports.api} Vite:{selectedSession.ports.vite}
160
- {selectedSession.ports.hmr ? ` HMR:${selectedSession.ports.hmr}` : ''}
161
- </Text>
162
- </Box>
163
-
164
- {/* Worktree */}
165
- {selectedSession.worktreePath && (
239
+ {selectedSession.ports.api > 0 && (
166
240
  <Box>
167
- <Text color="gray">Tree </Text>
168
- <Text color="white">{selectedSession.worktreePath}</Text>
241
+ <Text color="gray">Port </Text>
242
+ <Text color="white">
243
+ API:{selectedSession.ports.api} Vite:{selectedSession.ports.vite}
244
+ {selectedSession.ports.hmr ? ` HMR:${selectedSession.ports.hmr}` : ''}
245
+ </Text>
169
246
  </Box>
170
247
  )}
171
-
172
- {/* Wave association */}
173
- {selectedSession.waveId && (
248
+ {selectedSession.worktreePath && (
174
249
  <Box>
175
- <Text color="gray">Wave </Text>
176
- <Text color="white">
177
- {(() => {
178
- const wi = waves.findIndex(w => w.waveId === selectedSession.waveId);
179
- return wi >= 0 ? `Wave ${wi + 1}` : selectedSession.waveId;
180
- })()}
181
- </Text>
250
+ <Text color="gray">Tree </Text>
251
+ <Text color="white">{selectedSession.worktreePath}</Text>
182
252
  </Box>
183
253
  )}
184
-
185
- {/* Task */}
186
254
  {selectedSession.task && (
187
255
  <Box>
188
256
  <Text color="gray">Task </Text>
189
- <Text color="white">{selectedSession.task.slice(0, 60)}</Text>
257
+ <Text color="white" wrap="truncate">{selectedSession.task.slice(0, 60)}</Text>
190
258
  </Box>
191
259
  )}
192
-
193
260
  <Text color="gray">{'\u2500'.repeat(40)}</Text>
194
261
  </Box>
195
262
  )}
196
263
 
197
- {/* Agent Resource Header — role selected but no active session */}
198
264
  {selectedRoleId && !selectedSession && (
199
265
  <Box flexDirection="column" paddingX={1} marginBottom={0}>
200
266
  <Text bold color="cyan">{selectedRoleId}</Text>
201
- <Text color="gray">(no active session)</Text>
267
+ <Text color="gray">(not active in this wave)</Text>
202
268
  <Text color="gray">{'\u2500'.repeat(40)}</Text>
203
269
  </Box>
204
270
  )}
@@ -222,7 +288,7 @@ export const PanelMode: React.FC<PanelModeProps> = ({
222
288
  {/* Footer hints */}
223
289
  <Box paddingX={1} justifyContent="center">
224
290
  <Text color="gray" dimColor>
225
- [j/k] move [Enter] select [Esc] back to command
291
+ [j/k] move [Enter] all/role {waves.length > 1 ? '[1-9] wave ' : ''}[Esc] command [Ctrl+C] quit
226
292
  </Text>
227
293
  </Box>
228
294
  </Box>