tycono 0.3.11 → 0.3.12-beta.0
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
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -326,12 +326,17 @@ export const App: React.FC = () => {
|
|
|
326
326
|
return 'idle' as const;
|
|
327
327
|
}, [sse.streamStatus, activeCount]);
|
|
328
328
|
|
|
329
|
-
// Focused wave index (1-based)
|
|
329
|
+
// Focused wave index (1-based) + startedAt
|
|
330
330
|
const focusedWaveIndex = useMemo(() => {
|
|
331
331
|
if (!focusedWaveId) return 0;
|
|
332
332
|
return waves.findIndex(w => w.waveId === focusedWaveId) + 1;
|
|
333
333
|
}, [focusedWaveId, waves]);
|
|
334
334
|
|
|
335
|
+
const focusedWaveStartedAt = useMemo(() => {
|
|
336
|
+
if (!focusedWaveId) return 0;
|
|
337
|
+
return waves.find(w => w.waveId === focusedWaveId)?.startedAt ?? 0;
|
|
338
|
+
}, [focusedWaveId, waves]);
|
|
339
|
+
|
|
335
340
|
// Command handler
|
|
336
341
|
const { execute } = useCommand({
|
|
337
342
|
focusedWaveId,
|
|
@@ -375,12 +380,18 @@ export const App: React.FC = () => {
|
|
|
375
380
|
if (waves.length === 0) {
|
|
376
381
|
addSystemMessage('No waves.', 'gray');
|
|
377
382
|
} else {
|
|
383
|
+
// Show recent 8 waves max to prevent scrollback overflow
|
|
384
|
+
const MAX_SHOW = 8;
|
|
385
|
+
const showWaves = waves.length > MAX_SHOW ? waves.slice(-MAX_SHOW) : waves;
|
|
386
|
+
const skipped = waves.length - showWaves.length;
|
|
387
|
+
if (skipped > 0) addSystemMessage(`(+${skipped} older waves)`, 'gray');
|
|
378
388
|
addSystemMessage('Waves:', 'cyan');
|
|
379
|
-
|
|
389
|
+
showWaves.forEach((w) => {
|
|
390
|
+
const idx = waves.indexOf(w);
|
|
380
391
|
const isFocused = w.waveId === focusedWaveId;
|
|
381
392
|
const prefix = isFocused ? '*' : ' ';
|
|
382
393
|
const label = w.directive ? w.directive.slice(0, 60) : '(idle)';
|
|
383
|
-
addSystemMessage(`${prefix}${
|
|
394
|
+
addSystemMessage(`${prefix}${idx + 1}. Wave ${idx + 1} \u2014 ${label}`, isFocused ? 'green' : 'white');
|
|
384
395
|
});
|
|
385
396
|
}
|
|
386
397
|
break;
|
|
@@ -630,6 +641,8 @@ export const App: React.FC = () => {
|
|
|
630
641
|
onQuickAction={(action) => {
|
|
631
642
|
handleCommandSubmit(`/${action}`);
|
|
632
643
|
}}
|
|
644
|
+
activeSessions={api.activeSessions}
|
|
645
|
+
focusedWaveId={focusedWaveId}
|
|
633
646
|
/>
|
|
634
647
|
<StatusBar
|
|
635
648
|
companyName={api.company?.name ?? 'Loading...'}
|
|
@@ -639,6 +652,9 @@ export const App: React.FC = () => {
|
|
|
639
652
|
activeCount={activeCount}
|
|
640
653
|
portCount={api.portSummary.totalPorts}
|
|
641
654
|
totalCost={0}
|
|
655
|
+
activeSessions={api.activeSessions}
|
|
656
|
+
focusedWaveId={focusedWaveId}
|
|
657
|
+
waveStartedAt={focusedWaveStartedAt}
|
|
642
658
|
/>
|
|
643
659
|
</Box>
|
|
644
660
|
);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import React, { useState, useCallback, useRef } from 'react';
|
|
9
9
|
import { Box, Text, Static, useInput } from 'ink';
|
|
10
10
|
import TextInput from 'ink-text-input';
|
|
11
|
-
import type { SSEEvent } from '../api';
|
|
11
|
+
import type { SSEEvent, ActiveSessionInfo } from '../api';
|
|
12
12
|
import { getRoleColor } from '../theme';
|
|
13
13
|
// Markdown rendering is done inline via regex (no external dependency)
|
|
14
14
|
|
|
@@ -30,6 +30,8 @@ interface CommandModeProps {
|
|
|
30
30
|
systemMessages: StreamLine[];
|
|
31
31
|
onSubmit: (input: string) => void;
|
|
32
32
|
onQuickAction?: (action: string) => void;
|
|
33
|
+
activeSessions?: ActiveSessionInfo[];
|
|
34
|
+
focusedWaveId?: string | null;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
let lineCounter = 0;
|
|
@@ -290,6 +292,8 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
290
292
|
systemMessages,
|
|
291
293
|
onSubmit,
|
|
292
294
|
onQuickAction,
|
|
295
|
+
activeSessions,
|
|
296
|
+
focusedWaveId,
|
|
293
297
|
}) => {
|
|
294
298
|
const [input, setInput] = useState('');
|
|
295
299
|
const committedRef = useRef(0);
|
|
@@ -297,9 +301,15 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
297
301
|
const [quickBarActive, setQuickBarActive] = useState(false);
|
|
298
302
|
const [quickBarIndex, setQuickBarIndex] = useState(0);
|
|
299
303
|
|
|
300
|
-
// Convert events to stream lines
|
|
304
|
+
// Convert events to stream lines (collapse consecutive thinking from same role)
|
|
301
305
|
const eventLines: StreamLine[] = [];
|
|
302
|
-
for (
|
|
306
|
+
for (let i = 0; i < events.length; i++) {
|
|
307
|
+
const event = events[i];
|
|
308
|
+
// Skip thinking events if next event from same role is also thinking
|
|
309
|
+
if (event.type === 'thinking' && i + 1 < events.length) {
|
|
310
|
+
const next = events[i + 1];
|
|
311
|
+
if (next.type === 'thinking' && next.roleId === event.roleId) continue;
|
|
312
|
+
}
|
|
303
313
|
const line = summarizeEvent(event, allRoleIds);
|
|
304
314
|
if (line) eventLines.push(line);
|
|
305
315
|
}
|
|
@@ -387,6 +397,25 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
387
397
|
</Box>
|
|
388
398
|
)}
|
|
389
399
|
|
|
400
|
+
{/* Live mini-tree — shows active roles during dispatch */}
|
|
401
|
+
{(() => {
|
|
402
|
+
if (!activeSessions || !focusedWaveId) return null;
|
|
403
|
+
const waveRoles = activeSessions
|
|
404
|
+
.filter(s => s.waveId === focusedWaveId && s.status === 'active')
|
|
405
|
+
.map(s => s.roleId);
|
|
406
|
+
const unique = [...new Set(waveRoles)];
|
|
407
|
+
if (unique.length === 0) return null;
|
|
408
|
+
return (
|
|
409
|
+
<Box paddingX={0}>
|
|
410
|
+
<Text color="gray">{unique.map((r, i) => {
|
|
411
|
+
const dot = '\u25CF';
|
|
412
|
+
const arrow = i < unique.length - 1 ? '\u2192' : '';
|
|
413
|
+
return `${dot}${r}${arrow}`;
|
|
414
|
+
}).join('')}</Text>
|
|
415
|
+
</Box>
|
|
416
|
+
);
|
|
417
|
+
})()}
|
|
418
|
+
|
|
390
419
|
{/* Input */}
|
|
391
420
|
<Box paddingX={0} marginTop={0}>
|
|
392
421
|
<Text color={quickBarActive ? 'gray' : 'yellow'} bold>> </Text>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* StatusBar — bottom bar (Claude Code style)
|
|
3
|
-
* Shows: company name, wave
|
|
3
|
+
* Shows: company name, wave info, dispatch chain, active roles, elapsed, cost
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import { Box, Text } from 'ink';
|
|
8
|
+
import type { ActiveSessionInfo } from '../api';
|
|
8
9
|
|
|
9
10
|
interface StatusBarProps {
|
|
10
11
|
companyName: string;
|
|
@@ -14,6 +15,17 @@ interface StatusBarProps {
|
|
|
14
15
|
activeCount: number;
|
|
15
16
|
portCount: number; // total allocated ports
|
|
16
17
|
totalCost: number;
|
|
18
|
+
activeSessions?: ActiveSessionInfo[];
|
|
19
|
+
focusedWaveId?: string | null;
|
|
20
|
+
waveStartedAt?: number; // timestamp of focused wave start
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function elapsed(ms: number): string {
|
|
24
|
+
const s = Math.floor(ms / 1000);
|
|
25
|
+
if (s < 60) return `${s}s`;
|
|
26
|
+
const m = Math.floor(s / 60);
|
|
27
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
28
|
+
return `${Math.floor(m / 60)}h${m % 60}m`;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
export const StatusBar: React.FC<StatusBarProps> = ({
|
|
@@ -24,6 +36,9 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
24
36
|
activeCount,
|
|
25
37
|
portCount,
|
|
26
38
|
totalCost,
|
|
39
|
+
activeSessions,
|
|
40
|
+
focusedWaveId,
|
|
41
|
+
waveStartedAt,
|
|
27
42
|
}) => {
|
|
28
43
|
const statusDot = waveStatus === 'running' ? ' \u25CF'
|
|
29
44
|
: waveStatus === 'done' ? ' \u2713'
|
|
@@ -36,6 +51,25 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
36
51
|
// Show [1/3] only when 2+ waves
|
|
37
52
|
const countLabel = waveCount >= 2 ? ` [${waveIndex}/${waveCount}]` : '';
|
|
38
53
|
|
|
54
|
+
// Dispatch chain: show active roles for focused wave
|
|
55
|
+
let chainLabel = '';
|
|
56
|
+
if (activeSessions && focusedWaveId && waveStatus === 'running') {
|
|
57
|
+
const waveRoles = activeSessions
|
|
58
|
+
.filter(s => s.waveId === focusedWaveId && s.status === 'active')
|
|
59
|
+
.map(s => s.roleId);
|
|
60
|
+
// Deduplicate and show chain
|
|
61
|
+
const unique = [...new Set(waveRoles)];
|
|
62
|
+
if (unique.length > 0) {
|
|
63
|
+
chainLabel = unique.join('\u2192');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Elapsed time
|
|
68
|
+
let elapsedLabel = '';
|
|
69
|
+
if (waveStartedAt && waveStatus === 'running') {
|
|
70
|
+
elapsedLabel = elapsed(Date.now() - waveStartedAt);
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
return (
|
|
40
74
|
<Box width="100%" paddingX={1}>
|
|
41
75
|
<Text color="cyan" bold>Tycono</Text>
|
|
@@ -49,7 +83,19 @@ export const StatusBar: React.FC<StatusBarProps> = ({
|
|
|
49
83
|
</Text>
|
|
50
84
|
</>
|
|
51
85
|
)}
|
|
52
|
-
{
|
|
86
|
+
{chainLabel && (
|
|
87
|
+
<>
|
|
88
|
+
<Text color="gray"> | </Text>
|
|
89
|
+
<Text color="yellow">{chainLabel}</Text>
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
{elapsedLabel && (
|
|
93
|
+
<>
|
|
94
|
+
<Text color="gray"> </Text>
|
|
95
|
+
<Text color="gray">{elapsedLabel}</Text>
|
|
96
|
+
</>
|
|
97
|
+
)}
|
|
98
|
+
{!chainLabel && activeCount > 0 && (
|
|
53
99
|
<>
|
|
54
100
|
<Text color="gray"> | </Text>
|
|
55
101
|
<Text color="yellow">{activeCount} active</Text>
|