tycono 0.1.96-beta.30 → 0.1.96-beta.32
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/api/src/services/supervisor-heartbeat.ts +45 -11
- package/src/tui/app.tsx +11 -10
- package/src/tui/components/PanelMode.tsx +142 -76
package/package.json
CHANGED
|
@@ -264,12 +264,45 @@ class SupervisorHeartbeat {
|
|
|
264
264
|
? `\n\n⚠️ [RECOVERY] This is a restart after crash #${state.crashCount}. Check all session states via supervision watch.`
|
|
265
265
|
: '';
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
// Build conversation context from previous directives
|
|
268
|
+
const deliveredDirectives = state.pendingDirectives.filter(d => d.delivered);
|
|
269
|
+
const conversationHistory = deliveredDirectives.length > 0
|
|
270
|
+
? `\n## Previous Conversation in This Wave
|
|
271
|
+
${deliveredDirectives.map((d, i) => `${i + 1}. CEO said: "${d.text}"`).join('\n')}
|
|
272
|
+
|
|
273
|
+
You are continuing this conversation. The CEO's latest message builds on the above context.
|
|
274
|
+
Do NOT re-analyze from scratch — reference your previous findings.\n`
|
|
275
|
+
: '';
|
|
268
276
|
|
|
277
|
+
const supervisorTask = `[CEO Supervisor] ${state.directive}
|
|
278
|
+
${conversationHistory}
|
|
269
279
|
## Your Role
|
|
270
|
-
You are the CEO Supervisor — the
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
You are the CEO Supervisor — the CEO's AI proxy.
|
|
281
|
+
You can answer questions directly OR dispatch C-Level roles for complex work.
|
|
282
|
+
|
|
283
|
+
## Response Mode Decision (BEFORE dispatching)
|
|
284
|
+
|
|
285
|
+
⛔ Dispatch is expensive (spawns entire teams). Judge first:
|
|
286
|
+
|
|
287
|
+
**1. Direct Answer** — Can YOU handle this without dispatching?
|
|
288
|
+
- Status check, progress report → Read files/docs yourself, answer directly
|
|
289
|
+
- Simple question → Answer directly
|
|
290
|
+
- Opinion request → Answer directly
|
|
291
|
+
- Clarification on previous work → Answer from context
|
|
292
|
+
→ **Do NOT dispatch. Just answer.**
|
|
293
|
+
|
|
294
|
+
**2. Selective Dispatch** — Only specific C-Level(s) needed?
|
|
295
|
+
- "코드 수정해" → CTO only
|
|
296
|
+
- "디자인 개선해" → CBO only
|
|
297
|
+
- "테스트해봐" → CTO only (who dispatches QA)
|
|
298
|
+
→ **Dispatch only the relevant C-Level(s).**
|
|
299
|
+
|
|
300
|
+
**3. Full Dispatch** — Multi-team collaboration required?
|
|
301
|
+
- "새 기능 만들어" → CTO + CBO
|
|
302
|
+
- "출시 준비해" → All C-Levels
|
|
303
|
+
→ **Dispatch multiple C-Levels with clear tasks.**
|
|
304
|
+
|
|
305
|
+
**Default: Direct Answer first. Dispatch only when code changes or creative work is needed.**
|
|
273
306
|
|
|
274
307
|
## Available C-Level Roles
|
|
275
308
|
${cLevelList}
|
|
@@ -352,13 +385,14 @@ ${state.continuous ? `## Continuous Improvement Mode (ON)
|
|
|
352
385
|
5. 사용자가 Stop을 누를 때까지 계속한다. 스스로 done 선언하지 마라.
|
|
353
386
|
|
|
354
387
|
` : ''}## Instructions
|
|
355
|
-
1.
|
|
356
|
-
2.
|
|
357
|
-
3.
|
|
358
|
-
4.
|
|
359
|
-
5.
|
|
360
|
-
6.
|
|
361
|
-
7.
|
|
388
|
+
1. **First: Apply Response Mode Decision** — Can you answer directly? If yes, answer and report done.
|
|
389
|
+
2. If dispatch is needed: decide which C-Level roles (not necessarily all)
|
|
390
|
+
3. Dispatch with clear, specific tasks
|
|
391
|
+
4. Enter supervision watch loop
|
|
392
|
+
5. Monitor, **actively relay results between teams**, course-correct
|
|
393
|
+
6. When subordinates report done → **verify deliverables against requirements (G-09)**
|
|
394
|
+
7. If gaps exist → re-dispatch with specific feedback. Repeat 4-6.
|
|
395
|
+
8. Only when ALL requirements are met → compile results and report`;
|
|
362
396
|
|
|
363
397
|
// BUG-008 fix: Wave:Supervisor:Session = 1:1:1 invariant.
|
|
364
398
|
// Reuse existing session on restart instead of creating a new one.
|
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>
|