tycono 0.1.96-beta.19 → 0.1.96-beta.20
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 +4 -0
- package/src/tui/components/PanelMode.tsx +150 -7
package/package.json
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -427,6 +427,10 @@ export const App: React.FC = () => {
|
|
|
427
427
|
selectedRoleId={selectedRoleId}
|
|
428
428
|
streamStatus={sse.streamStatus}
|
|
429
429
|
waveId={focusedWaveId}
|
|
430
|
+
activeSessions={api.activeSessions}
|
|
431
|
+
waves={waves}
|
|
432
|
+
focusedWaveId={focusedWaveId}
|
|
433
|
+
portSummary={api.portSummary}
|
|
430
434
|
onMove={(dir) => {
|
|
431
435
|
if (dir === 'up') {
|
|
432
436
|
setSelectedRoleIndex(Math.max(0, selectedRoleIndex - 1));
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PanelMode — Tab view: Org Tree (left) +
|
|
2
|
+
* PanelMode — Tab view: Org Tree (left) + Agent Detail + Stream (right)
|
|
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
|
|
3
8
|
*
|
|
4
9
|
* Navigation:
|
|
5
10
|
* j/k or arrow keys — move in Org Tree
|
|
@@ -12,7 +17,8 @@ import { Box, Text, useInput } from 'ink';
|
|
|
12
17
|
import { OrgTree } from './OrgTree';
|
|
13
18
|
import { StreamView } from './StreamView';
|
|
14
19
|
import type { OrgNode } from '../store';
|
|
15
|
-
import type { SSEEvent } from '../api';
|
|
20
|
+
import type { SSEEvent, ActiveSessionInfo } from '../api';
|
|
21
|
+
import type { WaveInfo } from '../hooks/useCommand';
|
|
16
22
|
|
|
17
23
|
interface PanelModeProps {
|
|
18
24
|
tree: OrgNode[];
|
|
@@ -22,11 +28,31 @@ interface PanelModeProps {
|
|
|
22
28
|
selectedRoleId: string | null;
|
|
23
29
|
streamStatus: 'idle' | 'streaming' | 'done' | 'error';
|
|
24
30
|
waveId: string | null;
|
|
31
|
+
activeSessions: ActiveSessionInfo[];
|
|
32
|
+
waves: WaveInfo[];
|
|
33
|
+
focusedWaveId: string | null;
|
|
34
|
+
portSummary: { active: number; totalPorts: number };
|
|
25
35
|
onMove: (direction: 'up' | 'down') => void;
|
|
26
36
|
onSelect: () => void;
|
|
27
37
|
onEscape: () => void;
|
|
28
38
|
}
|
|
29
39
|
|
|
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;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Format elapsed time */
|
|
49
|
+
function elapsed(startedAt: string): string {
|
|
50
|
+
const ms = Date.now() - new Date(startedAt).getTime();
|
|
51
|
+
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
|
52
|
+
if (ms < 3600_000) return `${Math.floor(ms / 60_000)}m`;
|
|
53
|
+
return `${Math.floor(ms / 3600_000)}h`;
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export const PanelMode: React.FC<PanelModeProps> = ({
|
|
31
57
|
tree,
|
|
32
58
|
flatRoles,
|
|
@@ -35,6 +61,10 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
35
61
|
selectedRoleId,
|
|
36
62
|
streamStatus,
|
|
37
63
|
waveId,
|
|
64
|
+
activeSessions,
|
|
65
|
+
waves,
|
|
66
|
+
focusedWaveId,
|
|
67
|
+
portSummary,
|
|
38
68
|
onMove,
|
|
39
69
|
onSelect,
|
|
40
70
|
onEscape,
|
|
@@ -75,11 +105,30 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
75
105
|
? flatRoles.includes(selectedRoleId) ? selectedRoleId : 'All'
|
|
76
106
|
: 'All';
|
|
77
107
|
|
|
108
|
+
// Find resource info for selected role
|
|
109
|
+
const selectedSession = selectedRoleId
|
|
110
|
+
? findSessionForRole(activeSessions, selectedRoleId)
|
|
111
|
+
: null;
|
|
112
|
+
|
|
113
|
+
// Focused wave info
|
|
114
|
+
const focusedWave = waves.find(w => w.waveId === focusedWaveId);
|
|
115
|
+
const focusedWaveIndex = focusedWaveId
|
|
116
|
+
? waves.findIndex(w => w.waveId === focusedWaveId) + 1
|
|
117
|
+
: 0;
|
|
118
|
+
|
|
119
|
+
// Count sessions per wave for summary
|
|
120
|
+
const waveSessionCounts = new Map<string, number>();
|
|
121
|
+
for (const s of activeSessions) {
|
|
122
|
+
if (s.waveId) {
|
|
123
|
+
waveSessionCounts.set(s.waveId, (waveSessionCounts.get(s.waveId) ?? 0) + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
78
127
|
return (
|
|
79
128
|
<Box flexDirection="column" flexGrow={1}>
|
|
80
|
-
{/* Main content: Org Tree left | Stream right */}
|
|
129
|
+
{/* Main content: Org Tree left | Detail + Stream right */}
|
|
81
130
|
<Box flexGrow={1}>
|
|
82
|
-
{/* Left: Org Tree */}
|
|
131
|
+
{/* Left: Org Tree + Resource Summary */}
|
|
83
132
|
<Box flexDirection="column" width={28}>
|
|
84
133
|
<OrgTree
|
|
85
134
|
tree={tree}
|
|
@@ -87,15 +136,109 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
87
136
|
selectedIndex={selectedRoleIndex}
|
|
88
137
|
flatRoles={flatRoles}
|
|
89
138
|
/>
|
|
139
|
+
|
|
140
|
+
{/* Resource Summary — below org tree */}
|
|
141
|
+
<Box flexDirection="column" paddingX={1} marginTop={1}>
|
|
142
|
+
<Text color="gray">{'\u2500'.repeat(24)}</Text>
|
|
143
|
+
|
|
144
|
+
{/* Waves */}
|
|
145
|
+
{waves.length > 0 && (
|
|
146
|
+
<Box flexDirection="column">
|
|
147
|
+
{waves.map((w, i) => {
|
|
148
|
+
const isFocused = w.waveId === focusedWaveId;
|
|
149
|
+
const count = waveSessionCounts.get(w.waveId) ?? 0;
|
|
150
|
+
return (
|
|
151
|
+
<Box key={w.waveId}>
|
|
152
|
+
<Text color={isFocused ? 'green' : 'gray'}>
|
|
153
|
+
{isFocused ? '\u25B8' : ' '} W{i + 1}
|
|
154
|
+
</Text>
|
|
155
|
+
<Text color="gray"> {count > 0 ? `${count} agents` : 'idle'}</Text>
|
|
156
|
+
</Box>
|
|
157
|
+
);
|
|
158
|
+
})}
|
|
159
|
+
</Box>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{/* Port summary */}
|
|
163
|
+
{portSummary.totalPorts > 0 && (
|
|
164
|
+
<Box marginTop={0}>
|
|
165
|
+
<Text color="blue">{portSummary.totalPorts} ports</Text>
|
|
166
|
+
<Text color="gray"> allocated</Text>
|
|
167
|
+
</Box>
|
|
168
|
+
)}
|
|
169
|
+
</Box>
|
|
90
170
|
</Box>
|
|
91
171
|
|
|
92
172
|
{/* Vertical separator — fill available height */}
|
|
93
173
|
<Box flexDirection="column" marginX={0}>
|
|
94
|
-
<Text color="gray">{'
|
|
174
|
+
<Text color="gray">{'\u2502\n'.repeat(Math.max(5, termHeight - 6))}</Text>
|
|
95
175
|
</Box>
|
|
96
176
|
|
|
97
|
-
{/* Right:
|
|
177
|
+
{/* Right: Agent Detail + Stream */}
|
|
98
178
|
<Box flexGrow={1} flexDirection="column" overflow="hidden">
|
|
179
|
+
{/* Agent Resource Header — shown when a role is selected */}
|
|
180
|
+
{selectedRoleId && selectedSession && (
|
|
181
|
+
<Box flexDirection="column" paddingX={1} marginBottom={0}>
|
|
182
|
+
<Box justifyContent="space-between">
|
|
183
|
+
<Text bold color="cyan">{selectedRoleId}</Text>
|
|
184
|
+
<Text color={selectedSession.status === 'active' ? 'green' : 'gray'}>
|
|
185
|
+
{selectedSession.status === 'active' ? '\u25CF' : '\u25CB'} {selectedSession.status}
|
|
186
|
+
{selectedSession.startedAt ? ` (${elapsed(selectedSession.startedAt)})` : ''}
|
|
187
|
+
</Text>
|
|
188
|
+
</Box>
|
|
189
|
+
|
|
190
|
+
{/* Ports */}
|
|
191
|
+
<Box>
|
|
192
|
+
<Text color="gray">Port </Text>
|
|
193
|
+
<Text color="white">
|
|
194
|
+
API:{selectedSession.ports.api} Vite:{selectedSession.ports.vite}
|
|
195
|
+
{selectedSession.ports.hmr ? ` HMR:${selectedSession.ports.hmr}` : ''}
|
|
196
|
+
</Text>
|
|
197
|
+
</Box>
|
|
198
|
+
|
|
199
|
+
{/* Worktree */}
|
|
200
|
+
{selectedSession.worktreePath && (
|
|
201
|
+
<Box>
|
|
202
|
+
<Text color="gray">Tree </Text>
|
|
203
|
+
<Text color="white">{selectedSession.worktreePath}</Text>
|
|
204
|
+
</Box>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{/* Wave association */}
|
|
208
|
+
{selectedSession.waveId && (
|
|
209
|
+
<Box>
|
|
210
|
+
<Text color="gray">Wave </Text>
|
|
211
|
+
<Text color="white">
|
|
212
|
+
{(() => {
|
|
213
|
+
const wi = waves.findIndex(w => w.waveId === selectedSession.waveId);
|
|
214
|
+
return wi >= 0 ? `Wave ${wi + 1}` : selectedSession.waveId;
|
|
215
|
+
})()}
|
|
216
|
+
</Text>
|
|
217
|
+
</Box>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Task */}
|
|
221
|
+
{selectedSession.task && (
|
|
222
|
+
<Box>
|
|
223
|
+
<Text color="gray">Task </Text>
|
|
224
|
+
<Text color="white">{selectedSession.task.slice(0, 60)}</Text>
|
|
225
|
+
</Box>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
229
|
+
</Box>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Agent Resource Header — role selected but no active session */}
|
|
233
|
+
{selectedRoleId && !selectedSession && (
|
|
234
|
+
<Box flexDirection="column" paddingX={1} marginBottom={0}>
|
|
235
|
+
<Text bold color="cyan">{selectedRoleId}</Text>
|
|
236
|
+
<Text color="gray">(no active session)</Text>
|
|
237
|
+
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
238
|
+
</Box>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
{/* Stream */}
|
|
99
242
|
<StreamView
|
|
100
243
|
events={roleEvents}
|
|
101
244
|
allRoleIds={flatRoles}
|
|
@@ -108,7 +251,7 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
108
251
|
|
|
109
252
|
{/* Separator */}
|
|
110
253
|
<Box width="100%">
|
|
111
|
-
<Text color="gray">{'
|
|
254
|
+
<Text color="gray">{'\u2500'.repeat(process.stdout.columns || 70)}</Text>
|
|
112
255
|
</Box>
|
|
113
256
|
|
|
114
257
|
{/* Footer hints */}
|