tycono 0.1.96-beta.2 → 0.1.96-beta.21
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/README.md +106 -35
- package/bin/tycono.ts +23 -1
- package/package.json +1 -1
- package/src/api/src/routes/active-sessions.ts +3 -0
- package/src/api/src/routes/execute.ts +6 -12
- package/src/api/src/services/supervisor-heartbeat.ts +19 -0
- package/src/tui/api.ts +40 -7
- package/src/tui/app.tsx +369 -173
- package/src/tui/components/CommandMode.tsx +277 -0
- package/src/tui/components/OrgTree.tsx +4 -17
- package/src/tui/components/PanelMode.tsx +265 -0
- package/src/tui/components/SetupWizard.tsx +10 -3
- package/src/tui/components/StatusBar.tsx +44 -25
- package/src/tui/components/{StreamPanel.tsx → StreamView.tsx} +53 -73
- package/src/tui/hooks/useApi.ts +27 -6
- package/src/tui/hooks/useCommand.ts +162 -0
- package/src/tui/hooks/useSSE.ts +68 -24
- package/src/tui/store.ts +12 -0
- package/src/tui/components/CommandInput.tsx +0 -32
- package/src/tui/components/HelpOverlay.tsx +0 -51
- package/src/tui/components/SessionList.tsx +0 -74
- package/src/tui/components/WaveDialog.tsx +0 -56
- package/src/tui/hooks/useKeyboard.ts +0 -62
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommandMode — chat-first mode
|
|
3
|
+
*
|
|
4
|
+
* User = CEO. Supervisor (ceo role) = user's AI proxy.
|
|
5
|
+
* - Supervisor responses: shown directly (no prefix), like a conversation
|
|
6
|
+
* - Team activity: indented with roleId, concise
|
|
7
|
+
* - System prompts, internal noise: filtered out
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useCallback } from 'react';
|
|
11
|
+
import { Box, Text } from 'ink';
|
|
12
|
+
import TextInput from 'ink-text-input';
|
|
13
|
+
import type { SSEEvent } from '../api';
|
|
14
|
+
import { getRoleColor } from '../theme';
|
|
15
|
+
|
|
16
|
+
const MAX_STREAM_LINES = 30;
|
|
17
|
+
const SUPERVISOR_ROLE = 'ceo';
|
|
18
|
+
|
|
19
|
+
export interface StreamLine {
|
|
20
|
+
id: number;
|
|
21
|
+
text: string;
|
|
22
|
+
color: string;
|
|
23
|
+
prefix?: string;
|
|
24
|
+
prefixColor?: string;
|
|
25
|
+
indent?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CommandModeProps {
|
|
29
|
+
events: SSEEvent[];
|
|
30
|
+
allRoleIds: string[];
|
|
31
|
+
systemMessages: StreamLine[];
|
|
32
|
+
onSubmit: (input: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let lineCounter = 0;
|
|
36
|
+
|
|
37
|
+
/** Filter out system prompt noise from text */
|
|
38
|
+
function isSystemNoise(text: string): boolean {
|
|
39
|
+
const t = text.trim();
|
|
40
|
+
if (!t) return true;
|
|
41
|
+
// System prompt fragments
|
|
42
|
+
if (t.startsWith('## Your Role')) return true;
|
|
43
|
+
if (t.startsWith('You are')) return true;
|
|
44
|
+
if (t.startsWith('[CEO Supervisor]')) return true;
|
|
45
|
+
if (t.startsWith('[Question from')) return true;
|
|
46
|
+
if (t.includes('⛔ AKB Rule')) return true;
|
|
47
|
+
if (t.includes('⛔ Read the')) return true;
|
|
48
|
+
if (t.startsWith('⛔')) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Convert SSE event to stream lines */
|
|
53
|
+
export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLine | null {
|
|
54
|
+
const isSupervisor = event.roleId === SUPERVISOR_ROLE;
|
|
55
|
+
const roleColor = getRoleColor(event.roleId, allRoleIds);
|
|
56
|
+
|
|
57
|
+
switch (event.type) {
|
|
58
|
+
case 'text': {
|
|
59
|
+
const text = ((event.data.text as string) ?? '');
|
|
60
|
+
if (isSystemNoise(text)) return null;
|
|
61
|
+
|
|
62
|
+
if (isSupervisor) {
|
|
63
|
+
// Supervisor text → direct response (no prefix, generous length)
|
|
64
|
+
return {
|
|
65
|
+
id: ++lineCounter,
|
|
66
|
+
text: text.slice(0, 200),
|
|
67
|
+
color: 'white',
|
|
68
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
// Team text → indented with role prefix, concise
|
|
71
|
+
return {
|
|
72
|
+
id: ++lineCounter,
|
|
73
|
+
prefix: event.roleId,
|
|
74
|
+
prefixColor: roleColor,
|
|
75
|
+
text: text.slice(0, 80),
|
|
76
|
+
color: 'white',
|
|
77
|
+
indent: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'dispatch:start': {
|
|
83
|
+
const target = (event.data.targetRole as string) ?? '';
|
|
84
|
+
const task = ((event.data.task as string) ?? '');
|
|
85
|
+
// Filter out system prompt from task display
|
|
86
|
+
const cleanTask = task.replace(/⛔[^⛔]*⛔[^"]*/g, '').trim().slice(0, 50);
|
|
87
|
+
if (isSupervisor) {
|
|
88
|
+
return {
|
|
89
|
+
id: ++lineCounter,
|
|
90
|
+
text: `→ ${target} 배정${cleanTask ? ': ' + cleanTask : ''}`,
|
|
91
|
+
color: 'yellow',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id: ++lineCounter,
|
|
96
|
+
prefix: event.roleId,
|
|
97
|
+
prefixColor: roleColor,
|
|
98
|
+
text: `→ ${target} 배정`,
|
|
99
|
+
color: 'yellow',
|
|
100
|
+
indent: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'dispatch:done': {
|
|
105
|
+
const target = (event.data.targetRole as string) ?? '';
|
|
106
|
+
return {
|
|
107
|
+
id: ++lineCounter,
|
|
108
|
+
prefix: event.roleId,
|
|
109
|
+
prefixColor: roleColor,
|
|
110
|
+
text: `← ${target} 완료`,
|
|
111
|
+
color: 'yellow',
|
|
112
|
+
indent: !isSupervisor,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'tool:start': {
|
|
117
|
+
const toolName = (event.data.name as string) ?? 'tool';
|
|
118
|
+
const input = event.data.input;
|
|
119
|
+
let detail = '';
|
|
120
|
+
if (input && typeof input === 'object') {
|
|
121
|
+
const inp = input as Record<string, unknown>;
|
|
122
|
+
if (inp.file_path) detail = ` ${String(inp.file_path).split('/').pop()}`;
|
|
123
|
+
else if (inp.command) detail = ` ${String(inp.command).slice(0, 40)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isSupervisor) {
|
|
127
|
+
// Supervisor tool use → subtle
|
|
128
|
+
return {
|
|
129
|
+
id: ++lineCounter,
|
|
130
|
+
text: ` → ${toolName}${detail}`,
|
|
131
|
+
color: 'gray',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
id: ++lineCounter,
|
|
136
|
+
prefix: event.roleId,
|
|
137
|
+
prefixColor: roleColor,
|
|
138
|
+
text: `→ ${toolName}${detail}`,
|
|
139
|
+
color: 'gray',
|
|
140
|
+
indent: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'msg:start': {
|
|
145
|
+
if (isSupervisor) return null; // Hide supervisor start (noise)
|
|
146
|
+
const task = ((event.data.task as string) ?? '');
|
|
147
|
+
const cleanTask = task.replace(/⛔[^⛔]*⛔[^"]*/g, '').trim().slice(0, 40);
|
|
148
|
+
return {
|
|
149
|
+
id: ++lineCounter,
|
|
150
|
+
prefix: event.roleId,
|
|
151
|
+
prefixColor: roleColor,
|
|
152
|
+
text: `▶ ${cleanTask || 'started'}`,
|
|
153
|
+
color: 'green',
|
|
154
|
+
indent: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case 'msg:done': {
|
|
159
|
+
const turns = event.data.turns as number | undefined;
|
|
160
|
+
if (isSupervisor) return null; // Hide supervisor done
|
|
161
|
+
return {
|
|
162
|
+
id: ++lineCounter,
|
|
163
|
+
prefix: event.roleId,
|
|
164
|
+
prefixColor: roleColor,
|
|
165
|
+
text: `✓ done${turns ? ` (${turns} turns)` : ''}`,
|
|
166
|
+
color: 'green',
|
|
167
|
+
indent: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'msg:error': {
|
|
172
|
+
if (isSupervisor) return null; // Supervisor errors handled by system messages
|
|
173
|
+
const error = ((event.data.error as string) ?? '').slice(0, 60);
|
|
174
|
+
return {
|
|
175
|
+
id: ++lineCounter,
|
|
176
|
+
prefix: event.roleId,
|
|
177
|
+
prefixColor: roleColor,
|
|
178
|
+
text: `✗ ${error}`,
|
|
179
|
+
color: 'red',
|
|
180
|
+
indent: true,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'msg:awaiting_input':
|
|
185
|
+
return {
|
|
186
|
+
id: ++lineCounter,
|
|
187
|
+
text: isSupervisor ? '...' : ` ${event.roleId}: waiting`,
|
|
188
|
+
color: 'yellow',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Hidden
|
|
192
|
+
case 'thinking':
|
|
193
|
+
case 'heartbeat:tick':
|
|
194
|
+
case 'heartbeat:skip':
|
|
195
|
+
case 'prompt:assembled':
|
|
196
|
+
case 'trace:response':
|
|
197
|
+
case 'tool:result':
|
|
198
|
+
return null;
|
|
199
|
+
|
|
200
|
+
default:
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const CommandMode: React.FC<CommandModeProps> = ({
|
|
206
|
+
events,
|
|
207
|
+
allRoleIds,
|
|
208
|
+
systemMessages,
|
|
209
|
+
onSubmit,
|
|
210
|
+
}) => {
|
|
211
|
+
const [input, setInput] = useState('');
|
|
212
|
+
|
|
213
|
+
// Convert events to stream lines
|
|
214
|
+
const eventLines: StreamLine[] = [];
|
|
215
|
+
for (const event of events) {
|
|
216
|
+
const line = summarizeEvent(event, allRoleIds);
|
|
217
|
+
if (line) eventLines.push(line);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Merge system messages and event lines, show last MAX_STREAM_LINES
|
|
221
|
+
const allLines = [...systemMessages, ...eventLines].slice(-MAX_STREAM_LINES);
|
|
222
|
+
|
|
223
|
+
const handleSubmit = useCallback((value: string) => {
|
|
224
|
+
const trimmed = value.trim();
|
|
225
|
+
if (trimmed) {
|
|
226
|
+
onSubmit(trimmed);
|
|
227
|
+
}
|
|
228
|
+
setInput('');
|
|
229
|
+
}, [onSubmit]);
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
233
|
+
{/* Stream area */}
|
|
234
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
235
|
+
{allLines.length === 0 && (
|
|
236
|
+
<Box marginTop={1}>
|
|
237
|
+
<Text color="gray" dimColor>
|
|
238
|
+
Type a message to your AI team, or /help for commands.
|
|
239
|
+
</Text>
|
|
240
|
+
</Box>
|
|
241
|
+
)}
|
|
242
|
+
{allLines.map((line) => (
|
|
243
|
+
<Box key={line.id}>
|
|
244
|
+
{line.indent && <Text> </Text>}
|
|
245
|
+
{line.prefix && (
|
|
246
|
+
<Text color={line.prefixColor} bold>
|
|
247
|
+
{(line.prefix).padEnd(12)}
|
|
248
|
+
</Text>
|
|
249
|
+
)}
|
|
250
|
+
<Text color={line.color}>{line.text}</Text>
|
|
251
|
+
</Box>
|
|
252
|
+
))}
|
|
253
|
+
</Box>
|
|
254
|
+
|
|
255
|
+
{/* Separator */}
|
|
256
|
+
<Box width="100%">
|
|
257
|
+
<Text color="gray">{'─'.repeat(process.stdout.columns || 70)}</Text>
|
|
258
|
+
</Box>
|
|
259
|
+
|
|
260
|
+
{/* Input */}
|
|
261
|
+
<Box paddingX={1} justifyContent="space-between">
|
|
262
|
+
<Box>
|
|
263
|
+
<Text color="yellow" bold>> </Text>
|
|
264
|
+
<TextInput
|
|
265
|
+
value={input}
|
|
266
|
+
onChange={setInput}
|
|
267
|
+
onSubmit={handleSubmit}
|
|
268
|
+
placeholder=""
|
|
269
|
+
/>
|
|
270
|
+
</Box>
|
|
271
|
+
<Box>
|
|
272
|
+
<Text color="gray" dimColor>[Tab] panel</Text>
|
|
273
|
+
</Box>
|
|
274
|
+
</Box>
|
|
275
|
+
</Box>
|
|
276
|
+
);
|
|
277
|
+
};
|
|
@@ -32,7 +32,6 @@ function statusColor(status: string): string {
|
|
|
32
32
|
|
|
33
33
|
interface FlatEntry {
|
|
34
34
|
roleId: string;
|
|
35
|
-
name: string;
|
|
36
35
|
level: string;
|
|
37
36
|
status: string;
|
|
38
37
|
prefix: string;
|
|
@@ -47,15 +46,14 @@ function flattenTree(nodes: OrgNode[], prefix: string = '', isLast: boolean[] =
|
|
|
47
46
|
|
|
48
47
|
let linePrefix = '';
|
|
49
48
|
for (let j = 0; j < isLast.length; j++) {
|
|
50
|
-
linePrefix += isLast[j] ? ' ' : '
|
|
49
|
+
linePrefix += isLast[j] ? ' ' : '│ ';
|
|
51
50
|
}
|
|
52
51
|
linePrefix += isLast.length > 0 || i > 0 || nodes.length > 1
|
|
53
|
-
? (last ? '
|
|
52
|
+
? (last ? '└─ ' : '├─ ')
|
|
54
53
|
: '';
|
|
55
54
|
|
|
56
55
|
result.push({
|
|
57
56
|
roleId: node.role.id,
|
|
58
|
-
name: node.role.name || node.role.id,
|
|
59
57
|
level: node.role.level,
|
|
60
58
|
status: node.status,
|
|
61
59
|
prefix: linePrefix,
|
|
@@ -72,19 +70,9 @@ function flattenTree(nodes: OrgNode[], prefix: string = '', isLast: boolean[] =
|
|
|
72
70
|
export const OrgTree: React.FC<OrgTreeProps> = ({ tree, focused, selectedIndex, flatRoles }) => {
|
|
73
71
|
const entries = flattenTree(tree);
|
|
74
72
|
|
|
75
|
-
// Map flatRoles index to entries
|
|
76
|
-
const flatRoleIdToEntryIdx = new Map<number, number>();
|
|
77
|
-
let roleIdx = 0;
|
|
78
|
-
for (let i = 0; i < entries.length; i++) {
|
|
79
|
-
if (roleIdx < flatRoles.length && flatRoles[roleIdx] === entries[i].roleId) {
|
|
80
|
-
flatRoleIdToEntryIdx.set(roleIdx, i);
|
|
81
|
-
roleIdx++;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
73
|
return (
|
|
86
74
|
<Box flexDirection="column" paddingX={1}>
|
|
87
|
-
<Text bold color={focused ? 'cyan' : 'gray'}
|
|
75
|
+
<Text bold color={focused ? 'cyan' : 'gray'}>{'── Org Tree ──'}</Text>
|
|
88
76
|
<Box marginTop={1}>
|
|
89
77
|
<Text color="yellow" bold>{'\uD83D\uDC51'} CEO</Text>
|
|
90
78
|
</Box>
|
|
@@ -108,9 +96,8 @@ export const OrgTree: React.FC<OrgTreeProps> = ({ tree, focused, selectedIndex,
|
|
|
108
96
|
bold={isSelected}
|
|
109
97
|
inverse={isSelected}
|
|
110
98
|
>
|
|
111
|
-
{entry.
|
|
99
|
+
{entry.roleId}
|
|
112
100
|
</Text>
|
|
113
|
-
<Text color="gray" dimColor> {entry.roleId}</Text>
|
|
114
101
|
</Box>
|
|
115
102
|
);
|
|
116
103
|
})}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
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
|
|
8
|
+
*
|
|
9
|
+
* 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
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useState, useEffect } from 'react';
|
|
16
|
+
import { Box, Text, useInput } from 'ink';
|
|
17
|
+
import { OrgTree } from './OrgTree';
|
|
18
|
+
import { StreamView } from './StreamView';
|
|
19
|
+
import type { OrgNode } from '../store';
|
|
20
|
+
import type { SSEEvent, ActiveSessionInfo } from '../api';
|
|
21
|
+
import type { WaveInfo } from '../hooks/useCommand';
|
|
22
|
+
|
|
23
|
+
interface PanelModeProps {
|
|
24
|
+
tree: OrgNode[];
|
|
25
|
+
flatRoles: string[];
|
|
26
|
+
events: SSEEvent[];
|
|
27
|
+
selectedRoleIndex: number;
|
|
28
|
+
selectedRoleId: string | null;
|
|
29
|
+
streamStatus: 'idle' | 'streaming' | 'done' | 'error';
|
|
30
|
+
waveId: string | null;
|
|
31
|
+
activeSessions: ActiveSessionInfo[];
|
|
32
|
+
waves: WaveInfo[];
|
|
33
|
+
focusedWaveId: string | null;
|
|
34
|
+
portSummary: { active: number; totalPorts: number };
|
|
35
|
+
onMove: (direction: 'up' | 'down') => void;
|
|
36
|
+
onSelect: () => void;
|
|
37
|
+
onEscape: () => void;
|
|
38
|
+
}
|
|
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
|
+
|
|
56
|
+
export const PanelMode: React.FC<PanelModeProps> = ({
|
|
57
|
+
tree,
|
|
58
|
+
flatRoles,
|
|
59
|
+
events,
|
|
60
|
+
selectedRoleIndex,
|
|
61
|
+
selectedRoleId,
|
|
62
|
+
streamStatus,
|
|
63
|
+
waveId,
|
|
64
|
+
activeSessions,
|
|
65
|
+
waves,
|
|
66
|
+
focusedWaveId,
|
|
67
|
+
portSummary,
|
|
68
|
+
onMove,
|
|
69
|
+
onSelect,
|
|
70
|
+
onEscape,
|
|
71
|
+
}) => {
|
|
72
|
+
// Track terminal height for vertical separator
|
|
73
|
+
const [termHeight, setTermHeight] = useState(process.stdout.rows || 30);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const onResize = () => setTermHeight(process.stdout.rows || 30);
|
|
76
|
+
process.stdout.on('resize', onResize);
|
|
77
|
+
return () => { process.stdout.off('resize', onResize); };
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
useInput((input, key) => {
|
|
81
|
+
if (key.escape) {
|
|
82
|
+
onEscape();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.upArrow || input === 'k') {
|
|
86
|
+
onMove('up');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.downArrow || input === 'j') {
|
|
90
|
+
onMove('down');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.return) {
|
|
94
|
+
onSelect();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Filter events for selected role
|
|
100
|
+
const roleEvents = selectedRoleId
|
|
101
|
+
? events.filter((e) => e.roleId === selectedRoleId)
|
|
102
|
+
: events;
|
|
103
|
+
|
|
104
|
+
const roleLabel = selectedRoleId
|
|
105
|
+
? flatRoles.includes(selectedRoleId) ? selectedRoleId : 'All'
|
|
106
|
+
: 'All';
|
|
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
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
129
|
+
{/* Main content: Org Tree left | Detail + Stream right */}
|
|
130
|
+
<Box flexGrow={1}>
|
|
131
|
+
{/* Left: Org Tree + Resource Summary */}
|
|
132
|
+
<Box flexDirection="column" width={28}>
|
|
133
|
+
<OrgTree
|
|
134
|
+
tree={tree}
|
|
135
|
+
focused={true}
|
|
136
|
+
selectedIndex={selectedRoleIndex}
|
|
137
|
+
flatRoles={flatRoles}
|
|
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>
|
|
170
|
+
</Box>
|
|
171
|
+
|
|
172
|
+
{/* Vertical separator — fill available height */}
|
|
173
|
+
<Box flexDirection="column" marginX={0}>
|
|
174
|
+
<Text color="gray">{'\u2502\n'.repeat(Math.max(5, termHeight - 6))}</Text>
|
|
175
|
+
</Box>
|
|
176
|
+
|
|
177
|
+
{/* Right: Agent Detail + Stream */}
|
|
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 */}
|
|
242
|
+
<StreamView
|
|
243
|
+
events={roleEvents}
|
|
244
|
+
allRoleIds={flatRoles}
|
|
245
|
+
streamStatus={streamStatus}
|
|
246
|
+
waveId={waveId}
|
|
247
|
+
roleLabel={roleLabel}
|
|
248
|
+
/>
|
|
249
|
+
</Box>
|
|
250
|
+
</Box>
|
|
251
|
+
|
|
252
|
+
{/* Separator */}
|
|
253
|
+
<Box width="100%">
|
|
254
|
+
<Text color="gray">{'\u2500'.repeat(process.stdout.columns || 70)}</Text>
|
|
255
|
+
</Box>
|
|
256
|
+
|
|
257
|
+
{/* Footer hints */}
|
|
258
|
+
<Box paddingX={1} justifyContent="center">
|
|
259
|
+
<Text color="gray" dimColor>
|
|
260
|
+
[j/k] move [Enter] select [Esc] back to command
|
|
261
|
+
</Text>
|
|
262
|
+
</Box>
|
|
263
|
+
</Box>
|
|
264
|
+
);
|
|
265
|
+
};
|
|
@@ -76,11 +76,18 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
const handleCodeDirSubmit = async (value: string) => {
|
|
79
|
-
const
|
|
79
|
+
const path = await import('node:path');
|
|
80
|
+
const fs = await import('node:fs');
|
|
81
|
+
const dir = path.resolve(value.trim() || './code');
|
|
80
82
|
setCodeDir(dir);
|
|
81
83
|
setStep('creating');
|
|
82
84
|
|
|
83
85
|
try {
|
|
86
|
+
// Ensure code directory exists
|
|
87
|
+
if (!fs.existsSync(dir)) {
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
84
91
|
const result = await postSetupScaffold(companyName, selectedTeam?.id ?? 'minimal');
|
|
85
92
|
setResultPath(result.path ?? '');
|
|
86
93
|
setResultRoles(result.rolesCreated ?? 0);
|
|
@@ -104,7 +111,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|
|
104
111
|
paddingY={1}
|
|
105
112
|
width={56}
|
|
106
113
|
>
|
|
107
|
-
<Text bold color="cyan">{'
|
|
114
|
+
<Text bold color="cyan">{'───'} TYCONO Setup {'───'}</Text>
|
|
108
115
|
|
|
109
116
|
<Box marginTop={1}>
|
|
110
117
|
<Text color="gray">No company found. Let's set one up.</Text>
|
|
@@ -137,7 +144,7 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
|
|
|
137
144
|
{teams.length > 0 ? (
|
|
138
145
|
<SelectInput
|
|
139
146
|
items={teams.map((t) => ({
|
|
140
|
-
label: `${t.id} ${t.
|
|
147
|
+
label: `${t.id} ${t.roles.map(r => typeof r === 'string' ? r : r.name || r.id).join(', ')}`,
|
|
141
148
|
value: t.id,
|
|
142
149
|
}))}
|
|
143
150
|
onSelect={handleTeamSelect}
|