tycono 0.1.96-beta.35 → 0.1.96-beta.37
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 +61 -1
- package/src/tui/components/PanelMode.tsx +252 -106
- package/src/tui/hooks/useCommand.ts +15 -2
package/package.json
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -395,6 +395,64 @@ export const App: React.FC = () => {
|
|
|
395
395
|
api.refresh();
|
|
396
396
|
break;
|
|
397
397
|
}
|
|
398
|
+
case 'docs': {
|
|
399
|
+
// Extract written/edited files from SSE events in current wave
|
|
400
|
+
const writtenFiles = new Set<string>();
|
|
401
|
+
for (const event of sse.events) {
|
|
402
|
+
if (event.type === 'tool:start') {
|
|
403
|
+
const name = (event.data.name as string) ?? '';
|
|
404
|
+
const input = event.data.input as Record<string, unknown> | undefined;
|
|
405
|
+
if (['Write', 'Edit', 'NotebookEdit'].includes(name) && input?.file_path) {
|
|
406
|
+
writtenFiles.add(String(input.file_path));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (writtenFiles.size === 0) {
|
|
411
|
+
addSystemMessage('No files created/modified in this wave.', 'gray');
|
|
412
|
+
} else {
|
|
413
|
+
addSystemMessage(`Files in this wave (${writtenFiles.size}):`, 'cyan');
|
|
414
|
+
for (const f of writtenFiles) {
|
|
415
|
+
const short = f.split('/').slice(-3).join('/');
|
|
416
|
+
addSystemMessage(` ${short}`, 'white');
|
|
417
|
+
}
|
|
418
|
+
addSystemMessage(' /read <path> to preview | /open <path> to edit', 'gray');
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 'read_file': {
|
|
423
|
+
const filePath = result.message;
|
|
424
|
+
try {
|
|
425
|
+
const content = await import('node:fs').then(fs =>
|
|
426
|
+
fs.readFileSync(filePath, 'utf-8')
|
|
427
|
+
);
|
|
428
|
+
const lines = content.split('\n');
|
|
429
|
+
const preview = lines.slice(0, 30);
|
|
430
|
+
addSystemMessage(`\u2500\u2500 ${filePath.split('/').slice(-2).join('/')} \u2500\u2500`, 'cyan');
|
|
431
|
+
for (const line of preview) {
|
|
432
|
+
addSystemMessage(line, 'white');
|
|
433
|
+
}
|
|
434
|
+
if (lines.length > 30) {
|
|
435
|
+
addSystemMessage(` ... +${lines.length - 30} more lines (/open to see full)`, 'gray');
|
|
436
|
+
}
|
|
437
|
+
addSystemMessage('\u2500'.repeat(40), 'gray');
|
|
438
|
+
} catch (err) {
|
|
439
|
+
addSystemMessage(`Cannot read: ${err instanceof Error ? err.message : 'unknown'}`, 'red');
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case 'open_file': {
|
|
444
|
+
const filePath = result.message;
|
|
445
|
+
const editor = process.env.EDITOR || process.env.VISUAL || 'less';
|
|
446
|
+
try {
|
|
447
|
+
const { execSync } = await import('node:child_process');
|
|
448
|
+
execSync(`${editor} "${filePath}"`, { stdio: 'inherit' });
|
|
449
|
+
addSystemMessage(`Opened: ${filePath}`, 'green');
|
|
450
|
+
} catch {
|
|
451
|
+
// Fallback to /read
|
|
452
|
+
addSystemMessage(`Cannot open with ${editor}. Use /read instead.`, 'yellow');
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
398
456
|
case 'error':
|
|
399
457
|
addSystemMessage(result.message, 'red');
|
|
400
458
|
break;
|
|
@@ -404,11 +462,13 @@ export const App: React.FC = () => {
|
|
|
404
462
|
addSystemMessage(' /new [text] Create new wave', 'white');
|
|
405
463
|
addSystemMessage(' /waves List all waves', 'white');
|
|
406
464
|
addSystemMessage(' /focus <n> Switch to wave n', 'white');
|
|
465
|
+
addSystemMessage(' /docs Files created in this wave', 'white');
|
|
466
|
+
addSystemMessage(' /read <path> Preview file content', 'white');
|
|
467
|
+
addSystemMessage(' /open <path> Open in $EDITOR', 'white');
|
|
407
468
|
addSystemMessage(' /agents Wave \u2192 Role \u2192 Session tree', 'white');
|
|
408
469
|
addSystemMessage(' /sessions Sessions + ports (kill/cleanup)', 'white');
|
|
409
470
|
addSystemMessage(' /kill <id> Kill a session', 'white');
|
|
410
471
|
addSystemMessage(' /cleanup Remove dead sessions', 'white');
|
|
411
|
-
addSystemMessage(' /status Current status', 'white');
|
|
412
472
|
addSystemMessage(' /help This help', 'white');
|
|
413
473
|
addSystemMessage(' /quit Exit', 'white');
|
|
414
474
|
addSystemMessage('Keys: [Tab] team panel [1-9] wave [Esc] back [Ctrl+C] quit', 'gray');
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PanelMode — Wave-scoped team view
|
|
2
|
+
* PanelMode — Wave-scoped team view with right-panel tabs
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Right: Selected role's resources + stream
|
|
4
|
+
* Left: Wave title + Org Tree (wave-scoped) + Wave tabs
|
|
5
|
+
* Right: [Stream] [Docs] [Info] — tab switching with h/l
|
|
7
6
|
*
|
|
8
7
|
* Navigation:
|
|
9
|
-
* j/k — move in Org Tree (auto-selects)
|
|
8
|
+
* j/k — move in Org Tree (auto-selects) or scroll in Docs
|
|
9
|
+
* h/l — switch right panel tab
|
|
10
10
|
* 1-9 — switch wave focus
|
|
11
|
-
* Enter — toggle filtered/all
|
|
11
|
+
* Enter — Stream: toggle filtered/all | Docs: open in vim
|
|
12
12
|
* Esc — return to Command Mode
|
|
13
|
-
* Ctrl+C — quit
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
17
16
|
import { Box, Text, useInput } from 'ink';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import { execSync } from 'node:child_process';
|
|
18
19
|
import { OrgTree } from './OrgTree';
|
|
19
20
|
import { StreamView } from './StreamView';
|
|
20
21
|
import type { OrgNode } from '../store';
|
|
21
22
|
import type { SSEEvent, ActiveSessionInfo, SessionInfo } from '../api';
|
|
22
23
|
import type { WaveInfo } from '../hooks/useCommand';
|
|
23
24
|
|
|
25
|
+
type RightTab = 'stream' | 'docs' | 'info';
|
|
26
|
+
|
|
24
27
|
interface PanelModeProps {
|
|
25
28
|
tree: OrgNode[];
|
|
26
29
|
flatRoles: string[];
|
|
@@ -39,7 +42,6 @@ interface PanelModeProps {
|
|
|
39
42
|
onFocusWave: (waveId: string) => void;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
/** Get wave-scoped role statuses */
|
|
43
45
|
function getWaveScopedStatuses(
|
|
44
46
|
allSessions: SessionInfo[],
|
|
45
47
|
focusedWaveId: string | null,
|
|
@@ -48,30 +50,22 @@ function getWaveScopedStatuses(
|
|
|
48
50
|
const statuses: Record<string, string> = {};
|
|
49
51
|
for (const s of allSessions) {
|
|
50
52
|
if (s.waveId !== focusedWaveId) continue;
|
|
51
|
-
if (s.status === 'active')
|
|
52
|
-
|
|
53
|
-
} else if (!statuses[s.roleId]) {
|
|
54
|
-
statuses[s.roleId] = 'done';
|
|
55
|
-
}
|
|
53
|
+
if (s.status === 'active') statuses[s.roleId] = 'working';
|
|
54
|
+
else if (!statuses[s.roleId]) statuses[s.roleId] = 'done';
|
|
56
55
|
}
|
|
57
56
|
return statuses;
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
/** Find active session for a role in focused wave */
|
|
61
59
|
function findSessionForRole(
|
|
62
60
|
activeSessions: ActiveSessionInfo[],
|
|
63
61
|
allSessions: SessionInfo[],
|
|
64
62
|
roleId: string,
|
|
65
63
|
focusedWaveId: string | null,
|
|
66
64
|
): ActiveSessionInfo | null {
|
|
67
|
-
// First try: session with matching waveId
|
|
68
65
|
if (focusedWaveId) {
|
|
69
66
|
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
|
-
}
|
|
67
|
+
if (waveSes) return activeSessions.find(s => s.sessionId === waveSes.id) ?? null;
|
|
73
68
|
}
|
|
74
|
-
// Fallback: any active session for this role
|
|
75
69
|
return activeSessions.find(s => s.roleId === roleId && s.status === 'active') ?? null;
|
|
76
70
|
}
|
|
77
71
|
|
|
@@ -82,24 +76,41 @@ function elapsed(startedAt: string): string {
|
|
|
82
76
|
return `${Math.floor(ms / 3600_000)}h`;
|
|
83
77
|
}
|
|
84
78
|
|
|
79
|
+
/** Extract files created/modified in this wave from SSE events */
|
|
80
|
+
function extractWaveFiles(events: SSEEvent[]): string[] {
|
|
81
|
+
const files = new Set<string>();
|
|
82
|
+
for (const e of events) {
|
|
83
|
+
if (e.type === 'tool:start') {
|
|
84
|
+
const name = (e.data.name as string) ?? '';
|
|
85
|
+
const input = e.data.input as Record<string, unknown> | undefined;
|
|
86
|
+
if (['Write', 'Edit', 'NotebookEdit'].includes(name) && input?.file_path) {
|
|
87
|
+
files.add(String(input.file_path));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return Array.from(files);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read file preview (first N lines) */
|
|
95
|
+
function readFilePreview(filePath: string, maxLines: number): string[] {
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
98
|
+
return content.split('\n').slice(0, maxLines);
|
|
99
|
+
} catch {
|
|
100
|
+
return ['(cannot read file)'];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
export const PanelMode: React.FC<PanelModeProps> = ({
|
|
86
|
-
tree,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
selectedRoleIndex,
|
|
90
|
-
selectedRoleId,
|
|
91
|
-
streamStatus,
|
|
92
|
-
waveId,
|
|
93
|
-
activeSessions,
|
|
94
|
-
allSessions,
|
|
95
|
-
waves,
|
|
96
|
-
focusedWaveId,
|
|
97
|
-
onMove,
|
|
98
|
-
onSelect,
|
|
99
|
-
onEscape,
|
|
100
|
-
onFocusWave,
|
|
105
|
+
tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
|
|
106
|
+
streamStatus, waveId, activeSessions, allSessions, waves,
|
|
107
|
+
focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
|
|
101
108
|
}) => {
|
|
102
109
|
const [termHeight, setTermHeight] = useState(process.stdout.rows || 30);
|
|
110
|
+
const [rightTab, setRightTab] = useState<RightTab>('stream');
|
|
111
|
+
const [docsIndex, setDocsIndex] = useState(0);
|
|
112
|
+
const [docsScroll, setDocsScroll] = useState(0);
|
|
113
|
+
|
|
103
114
|
useEffect(() => {
|
|
104
115
|
const onResize = () => setTermHeight(process.stdout.rows || 30);
|
|
105
116
|
process.stdout.on('resize', onResize);
|
|
@@ -108,13 +119,11 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
108
119
|
|
|
109
120
|
const separatorStr = useMemo(() => '\u2502\n'.repeat(Math.max(5, termHeight - 8)), [termHeight]);
|
|
110
121
|
|
|
111
|
-
// Wave-scoped statuses for Org Tree
|
|
112
122
|
const waveScopedStatuses = useMemo(
|
|
113
123
|
() => getWaveScopedStatuses(allSessions, focusedWaveId),
|
|
114
124
|
[allSessions, focusedWaveId],
|
|
115
125
|
);
|
|
116
126
|
|
|
117
|
-
// Override tree node statuses with wave-scoped values
|
|
118
127
|
const waveScopedTree = useMemo(() => {
|
|
119
128
|
function scopeNode(node: OrgNode): OrgNode {
|
|
120
129
|
return {
|
|
@@ -126,11 +135,74 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
126
135
|
return tree.map(scopeNode);
|
|
127
136
|
}, [tree, waveScopedStatuses]);
|
|
128
137
|
|
|
138
|
+
// Files created in this wave
|
|
139
|
+
const waveFiles = useMemo(() => extractWaveFiles(events), [events]);
|
|
140
|
+
|
|
141
|
+
// File preview for selected doc
|
|
142
|
+
const selectedFile = waveFiles[docsIndex] ?? null;
|
|
143
|
+
const filePreview = useMemo(() => {
|
|
144
|
+
if (!selectedFile || rightTab !== 'docs') return [];
|
|
145
|
+
return readFilePreview(selectedFile, 100);
|
|
146
|
+
}, [selectedFile, rightTab]);
|
|
147
|
+
|
|
129
148
|
useInput((input, key) => {
|
|
130
149
|
if (key.escape) { onEscape(); return; }
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (key.
|
|
150
|
+
|
|
151
|
+
// h/l: switch right panel tab
|
|
152
|
+
if (input === 'h' || (key.leftArrow && rightTab !== 'stream')) {
|
|
153
|
+
const tabs: RightTab[] = ['stream', 'docs', 'info'];
|
|
154
|
+
const idx = tabs.indexOf(rightTab);
|
|
155
|
+
if (idx > 0) { setRightTab(tabs[idx - 1]); setDocsScroll(0); }
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (input === 'l' || (key.rightArrow && rightTab !== 'info')) {
|
|
159
|
+
const tabs: RightTab[] = ['stream', 'docs', 'info'];
|
|
160
|
+
const idx = tabs.indexOf(rightTab);
|
|
161
|
+
if (idx < tabs.length - 1) { setRightTab(tabs[idx + 1]); setDocsScroll(0); }
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// j/k: context-dependent
|
|
166
|
+
if (key.upArrow || input === 'k') {
|
|
167
|
+
if (rightTab === 'docs' && docsScroll > 0) {
|
|
168
|
+
setDocsScroll(s => Math.max(0, s - 3));
|
|
169
|
+
} else if (rightTab === 'stream') {
|
|
170
|
+
onMove('up');
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (key.downArrow || input === 'j') {
|
|
175
|
+
if (rightTab === 'docs') {
|
|
176
|
+
setDocsScroll(s => s + 3);
|
|
177
|
+
} else if (rightTab === 'stream') {
|
|
178
|
+
onMove('down');
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Tab key for cycling docs files
|
|
184
|
+
if (key.tab && rightTab === 'docs') {
|
|
185
|
+
setDocsIndex(i => (i + 1) % Math.max(1, waveFiles.length));
|
|
186
|
+
setDocsScroll(0);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Enter
|
|
191
|
+
if (key.return) {
|
|
192
|
+
if (rightTab === 'docs' && selectedFile) {
|
|
193
|
+
// Open in vim
|
|
194
|
+
const editor = process.env.EDITOR || 'vim';
|
|
195
|
+
try {
|
|
196
|
+
execSync(`${editor} "${selectedFile}"`, { stdio: 'inherit' });
|
|
197
|
+
} catch { /* user quit editor */ }
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (rightTab === 'stream') {
|
|
201
|
+
onSelect();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
134
206
|
// 1-9: wave switch
|
|
135
207
|
const num = parseInt(input, 10);
|
|
136
208
|
if (num >= 1 && num <= 9 && num <= waves.length) {
|
|
@@ -147,18 +219,15 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
147
219
|
? flatRoles.includes(selectedRoleId) ? selectedRoleId : 'All'
|
|
148
220
|
: 'All';
|
|
149
221
|
|
|
150
|
-
// Find resource info for selected role (wave-scoped)
|
|
151
222
|
const selectedSession = selectedRoleId
|
|
152
223
|
? findSessionForRole(activeSessions, allSessions, selectedRoleId, focusedWaveId)
|
|
153
224
|
: null;
|
|
154
225
|
|
|
155
|
-
// Focused wave info
|
|
156
226
|
const focusedWave = waves.find(w => w.waveId === focusedWaveId);
|
|
157
227
|
const focusedWaveIndex = focusedWaveId
|
|
158
228
|
? waves.findIndex(w => w.waveId === focusedWaveId) + 1
|
|
159
229
|
: 0;
|
|
160
230
|
|
|
161
|
-
// Wave session count for display
|
|
162
231
|
const waveSessionCount = focusedWaveId
|
|
163
232
|
? allSessions.filter(s => s.waveId === focusedWaveId).length
|
|
164
233
|
: 0;
|
|
@@ -167,42 +236,41 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
167
236
|
|
|
168
237
|
return (
|
|
169
238
|
<Box flexDirection="column" flexGrow={1}>
|
|
170
|
-
{/* Main content */}
|
|
171
239
|
<Box flexGrow={1}>
|
|
172
240
|
{/* Left: Wave title + Org Tree + Wave tabs */}
|
|
173
241
|
<Box flexDirection="column" width={leftWidth}>
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
{
|
|
242
|
+
<Box paddingX={1}>
|
|
243
|
+
<Text color="green" bold>W{focusedWaveIndex}</Text>
|
|
244
|
+
<Text color="gray"> </Text>
|
|
245
|
+
<Text color="white" wrap="truncate">
|
|
246
|
+
{focusedWave?.directive ? focusedWave.directive.slice(0, leftWidth - 6) : '(idle)'}
|
|
247
|
+
</Text>
|
|
248
|
+
</Box>
|
|
249
|
+
{waveSessionCount > 0 && (
|
|
250
|
+
<Box paddingX={1}>
|
|
251
|
+
<Text color="gray">{waveSessionCount} sessions</Text>
|
|
179
252
|
</Box>
|
|
180
253
|
)}
|
|
181
254
|
|
|
182
|
-
{/* Org Tree (wave-scoped statuses) */}
|
|
183
255
|
<OrgTree
|
|
184
256
|
tree={waveScopedTree}
|
|
185
|
-
focused={
|
|
257
|
+
focused={rightTab === 'stream'}
|
|
186
258
|
selectedIndex={selectedRoleIndex}
|
|
187
259
|
flatRoles={flatRoles}
|
|
188
260
|
ceoStatus={waveScopedStatuses['ceo'] ?? 'idle'}
|
|
189
261
|
/>
|
|
190
262
|
|
|
191
|
-
{/* Wave tabs at bottom — compact inline */}
|
|
192
263
|
{waves.length > 1 && (
|
|
193
264
|
<Box paddingX={1} marginTop={1}>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
</Text>
|
|
204
|
-
);
|
|
205
|
-
})}
|
|
265
|
+
{waves.map((w, i) => (
|
|
266
|
+
<Box key={w.waveId} marginRight={1}>
|
|
267
|
+
<Text
|
|
268
|
+
color={w.waveId === focusedWaveId ? 'green' : 'gray'}
|
|
269
|
+
bold={w.waveId === focusedWaveId}
|
|
270
|
+
inverse={w.waveId === focusedWaveId}
|
|
271
|
+
>{` ${i + 1} `}</Text>
|
|
272
|
+
</Box>
|
|
273
|
+
))}
|
|
206
274
|
</Box>
|
|
207
275
|
)}
|
|
208
276
|
</Box>
|
|
@@ -212,71 +280,149 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
212
280
|
<Text color="gray">{separatorStr}</Text>
|
|
213
281
|
</Box>
|
|
214
282
|
|
|
215
|
-
{/* Right:
|
|
283
|
+
{/* Right: Tabbed panel */}
|
|
216
284
|
<Box flexGrow={1} flexDirection="column" overflow="hidden">
|
|
217
|
-
{/*
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
<Box
|
|
221
|
-
<Text
|
|
222
|
-
|
|
223
|
-
{
|
|
224
|
-
{
|
|
285
|
+
{/* Tab bar */}
|
|
286
|
+
<Box paddingX={1} marginBottom={0}>
|
|
287
|
+
{(['stream', 'docs', 'info'] as RightTab[]).map(tab => (
|
|
288
|
+
<Box key={tab} marginRight={1}>
|
|
289
|
+
<Text
|
|
290
|
+
color={rightTab === tab ? 'cyan' : 'gray'}
|
|
291
|
+
bold={rightTab === tab}
|
|
292
|
+
inverse={rightTab === tab}
|
|
293
|
+
>
|
|
294
|
+
{` ${tab.charAt(0).toUpperCase() + tab.slice(1)} `}
|
|
225
295
|
</Text>
|
|
226
296
|
</Box>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
297
|
+
))}
|
|
298
|
+
<Text color="gray" dimColor> [h/l] switch</Text>
|
|
299
|
+
</Box>
|
|
300
|
+
|
|
301
|
+
{/* Stream tab */}
|
|
302
|
+
{rightTab === 'stream' && (
|
|
303
|
+
<>
|
|
304
|
+
{selectedRoleId && selectedSession && (
|
|
305
|
+
<Box flexDirection="column" paddingX={1}>
|
|
306
|
+
<Box justifyContent="space-between">
|
|
307
|
+
<Text bold color="cyan">{selectedRoleId}</Text>
|
|
308
|
+
<Text color={selectedSession.status === 'active' ? 'green' : 'gray'}>
|
|
309
|
+
{selectedSession.status === 'active' ? '\u25CF' : '\u25CB'} {selectedSession.status}
|
|
310
|
+
{selectedSession.startedAt ? ` (${elapsed(selectedSession.startedAt)})` : ''}
|
|
311
|
+
</Text>
|
|
312
|
+
</Box>
|
|
313
|
+
{selectedSession.ports.api > 0 && (
|
|
314
|
+
<Text color="gray">Port API:{selectedSession.ports.api} Vite:{selectedSession.ports.vite}</Text>
|
|
315
|
+
)}
|
|
316
|
+
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
234
317
|
</Box>
|
|
235
318
|
)}
|
|
236
|
-
{selectedSession
|
|
237
|
-
<Box>
|
|
238
|
-
<Text color="
|
|
239
|
-
<Text color="
|
|
319
|
+
{selectedRoleId && !selectedSession && (
|
|
320
|
+
<Box flexDirection="column" paddingX={1}>
|
|
321
|
+
<Text bold color="cyan">{selectedRoleId}</Text>
|
|
322
|
+
<Text color="gray">(not active in this wave)</Text>
|
|
323
|
+
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
240
324
|
</Box>
|
|
241
325
|
)}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
326
|
+
<StreamView
|
|
327
|
+
events={roleEvents}
|
|
328
|
+
allRoleIds={flatRoles}
|
|
329
|
+
streamStatus={streamStatus}
|
|
330
|
+
waveId={waveId}
|
|
331
|
+
roleLabel={roleLabel}
|
|
332
|
+
/>
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{/* Docs tab */}
|
|
337
|
+
{rightTab === 'docs' && (
|
|
338
|
+
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
339
|
+
{waveFiles.length === 0 ? (
|
|
340
|
+
<Text color="gray">No files created in this wave yet.</Text>
|
|
341
|
+
) : (
|
|
342
|
+
<>
|
|
343
|
+
{/* File list */}
|
|
344
|
+
<Box marginBottom={1}>
|
|
345
|
+
<Text color="gray">Files ({waveFiles.length}): </Text>
|
|
346
|
+
{waveFiles.map((f, i) => (
|
|
347
|
+
<Box key={f} marginRight={1}>
|
|
348
|
+
<Text
|
|
349
|
+
color={i === docsIndex ? 'cyan' : 'gray'}
|
|
350
|
+
bold={i === docsIndex}
|
|
351
|
+
inverse={i === docsIndex}
|
|
352
|
+
>
|
|
353
|
+
{` ${f.split('/').pop()} `}
|
|
354
|
+
</Text>
|
|
355
|
+
</Box>
|
|
356
|
+
))}
|
|
357
|
+
<Text color="gray" dimColor> [Tab] next</Text>
|
|
358
|
+
</Box>
|
|
359
|
+
|
|
360
|
+
{/* File preview */}
|
|
361
|
+
{selectedFile && (
|
|
362
|
+
<Box flexDirection="column">
|
|
363
|
+
<Text color="cyan" bold>{selectedFile.split('/').slice(-2).join('/')}</Text>
|
|
364
|
+
<Text color="gray">{'\u2500'.repeat(50)}</Text>
|
|
365
|
+
{filePreview.slice(docsScroll, docsScroll + termHeight - 12).map((line, i) => (
|
|
366
|
+
<Text key={i} color="white" wrap="wrap">{line}</Text>
|
|
367
|
+
))}
|
|
368
|
+
{filePreview.length > termHeight - 12 && (
|
|
369
|
+
<Text color="gray" dimColor>
|
|
370
|
+
{docsScroll > 0 ? '\u2191 ' : ''}j/k scroll | {filePreview.length - docsScroll} lines remaining
|
|
371
|
+
</Text>
|
|
372
|
+
)}
|
|
373
|
+
</Box>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
<Box marginTop={1}>
|
|
377
|
+
<Text color="gray" dimColor>[Enter] open in {process.env.EDITOR || 'vim'} | [Tab] next file | [j/k] scroll</Text>
|
|
378
|
+
</Box>
|
|
379
|
+
</>
|
|
247
380
|
)}
|
|
248
|
-
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
249
381
|
</Box>
|
|
250
382
|
)}
|
|
251
383
|
|
|
252
|
-
{
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<Text color="
|
|
384
|
+
{/* Info tab */}
|
|
385
|
+
{rightTab === 'info' && (
|
|
386
|
+
<Box flexDirection="column" paddingX={1}>
|
|
387
|
+
<Text bold color="cyan">Wave Info</Text>
|
|
256
388
|
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
389
|
+
<Text color="white">Wave: {focusedWave?.waveId ?? 'none'}</Text>
|
|
390
|
+
<Text color="white">Directive: {focusedWave?.directive || '(idle)'}</Text>
|
|
391
|
+
<Text color="white">Sessions: {waveSessionCount}</Text>
|
|
392
|
+
<Text color="white">Files modified: {waveFiles.length}</Text>
|
|
393
|
+
<Text color="white">SSE events: {events.length}</Text>
|
|
394
|
+
|
|
395
|
+
{/* Active sessions in this wave */}
|
|
396
|
+
{waveSessionCount > 0 && (
|
|
397
|
+
<>
|
|
398
|
+
<Text color="gray" bold>{'\n'}Active in this wave:</Text>
|
|
399
|
+
{allSessions
|
|
400
|
+
.filter(s => s.waveId === focusedWaveId && s.status === 'active')
|
|
401
|
+
.slice(0, 10)
|
|
402
|
+
.map(s => {
|
|
403
|
+
const port = activeSessions.find(a => a.sessionId === s.id);
|
|
404
|
+
return (
|
|
405
|
+
<Text key={s.id} color="white">
|
|
406
|
+
{` ${s.roleId.padEnd(12)} ${s.id.slice(0, 20)} ${port ? `API:${port.ports.api}` : ''}`}
|
|
407
|
+
</Text>
|
|
408
|
+
);
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
</>
|
|
412
|
+
)}
|
|
257
413
|
</Box>
|
|
258
414
|
)}
|
|
259
|
-
|
|
260
|
-
{/* Stream */}
|
|
261
|
-
<StreamView
|
|
262
|
-
events={roleEvents}
|
|
263
|
-
allRoleIds={flatRoles}
|
|
264
|
-
streamStatus={streamStatus}
|
|
265
|
-
waveId={waveId}
|
|
266
|
-
roleLabel={roleLabel}
|
|
267
|
-
/>
|
|
268
415
|
</Box>
|
|
269
416
|
</Box>
|
|
270
417
|
|
|
271
|
-
{/*
|
|
418
|
+
{/* Footer */}
|
|
272
419
|
<Box width="100%">
|
|
273
420
|
<Text color="gray">{'\u2500'.repeat(process.stdout.columns || 70)}</Text>
|
|
274
421
|
</Box>
|
|
275
|
-
|
|
276
|
-
{/* Footer hints */}
|
|
277
422
|
<Box paddingX={1} justifyContent="center">
|
|
278
423
|
<Text color="gray" dimColor>
|
|
279
|
-
[
|
|
424
|
+
[h/l] tab [j/k] {rightTab === 'stream' ? 'role' : 'scroll'} {rightTab === 'docs' ? '[Enter] vim ' : ''}
|
|
425
|
+
{waves.length > 1 ? '[1-9] wave ' : ''}[Esc] command
|
|
280
426
|
</Text>
|
|
281
427
|
</Box>
|
|
282
428
|
</Box>
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { useCallback } from 'react';
|
|
19
|
-
import { dispatchWave, sendDirective, fetchJson, killSession, cleanupSessions } from '../api';
|
|
19
|
+
import { dispatchWave, sendDirective, fetchJson, killSession, cleanupSessions, fetchActiveSessions } from '../api';
|
|
20
20
|
|
|
21
21
|
export interface WaveInfo {
|
|
22
22
|
waveId: string;
|
|
@@ -25,7 +25,7 @@ export interface WaveInfo {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface CommandResult {
|
|
28
|
-
type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel' | 'waves_list' | 'focus_changed' | 'agents' | 'ports' | 'sessions' | 'cleanup';
|
|
28
|
+
type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel' | 'waves_list' | 'focus_changed' | 'agents' | 'ports' | 'sessions' | 'cleanup' | 'docs' | 'read_file' | 'open_file';
|
|
29
29
|
message: string;
|
|
30
30
|
waveId?: string;
|
|
31
31
|
}
|
|
@@ -123,6 +123,19 @@ export function useCommand(options: UseCommandOptions) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
case 'docs':
|
|
127
|
+
return { type: 'docs', message: '__docs__' };
|
|
128
|
+
|
|
129
|
+
case 'read': {
|
|
130
|
+
if (!args) return { type: 'error', message: 'Usage: /read <file_path>' };
|
|
131
|
+
return { type: 'read_file', message: args.trim() };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'open': {
|
|
135
|
+
if (!args) return { type: 'error', message: 'Usage: /open <file_path>' };
|
|
136
|
+
return { type: 'open_file', message: args.trim() };
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
case 'status':
|
|
127
140
|
return { type: 'info', message: '__status__' };
|
|
128
141
|
|