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 +1 -1
- package/src/tui/app.tsx +11 -10
- package/src/tui/components/PanelMode.tsx +142 -76
package/package.json
CHANGED
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
|
|
408
|
-
addSystemMessage(' /ports
|
|
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
|
|
413
|
-
addSystemMessage(' /
|
|
414
|
-
addSystemMessage(' /
|
|
415
|
-
addSystemMessage('
|
|
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 —
|
|
2
|
+
* PanelMode — Wave-scoped team view
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
/**
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
170
|
+
{/* Main content */}
|
|
125
171
|
<Box flexGrow={1}>
|
|
126
|
-
{/* Left: Org Tree +
|
|
127
|
-
<Box flexDirection="column" width={
|
|
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={
|
|
194
|
+
tree={waveScopedTree}
|
|
130
195
|
focused={true}
|
|
131
196
|
selectedIndex={selectedRoleIndex}
|
|
132
197
|
flatRoles={flatRoles}
|
|
133
|
-
ceoStatus={
|
|
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
|
|
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
|
|
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">
|
|
168
|
-
<Text color="white">
|
|
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">
|
|
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">(
|
|
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]
|
|
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>
|