tycono 0.1.96-beta.24 → 0.1.96-beta.26
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
|
@@ -67,7 +67,7 @@ function flattenTree(nodes: OrgNode[], prefix: string = '', isLast: boolean[] =
|
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export const OrgTree: React.FC<OrgTreeProps> = ({ tree, focused, selectedIndex, flatRoles }) => {
|
|
70
|
+
export const OrgTree: React.FC<OrgTreeProps> = React.memo(({ tree, focused, selectedIndex, flatRoles }) => {
|
|
71
71
|
const entries = flattenTree(tree);
|
|
72
72
|
|
|
73
73
|
return (
|
|
@@ -103,4 +103,4 @@ export const OrgTree: React.FC<OrgTreeProps> = ({ tree, focused, selectedIndex,
|
|
|
103
103
|
})}
|
|
104
104
|
</Box>
|
|
105
105
|
);
|
|
106
|
-
};
|
|
106
|
+
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Esc — return to Command Mode
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import React, { useState, useEffect } from 'react';
|
|
15
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
16
16
|
import { Box, Text, useInput } from 'ink';
|
|
17
17
|
import { OrgTree } from './OrgTree';
|
|
18
18
|
import { StreamView } from './StreamView';
|
|
@@ -77,6 +77,9 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
77
77
|
return () => { process.stdout.off('resize', onResize); };
|
|
78
78
|
}, []);
|
|
79
79
|
|
|
80
|
+
// Memoize expensive strings
|
|
81
|
+
const separatorStr = useMemo(() => '\u2502\n'.repeat(Math.max(5, termHeight - 6)), [termHeight]);
|
|
82
|
+
|
|
80
83
|
useInput((input, key) => {
|
|
81
84
|
if (key.escape) {
|
|
82
85
|
onEscape();
|
|
@@ -116,14 +119,6 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
116
119
|
? waves.findIndex(w => w.waveId === focusedWaveId) + 1
|
|
117
120
|
: 0;
|
|
118
121
|
|
|
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
122
|
return (
|
|
128
123
|
<Box flexDirection="column" flexGrow={1}>
|
|
129
124
|
{/* Main content: Org Tree left | Detail + Stream right */}
|
|
@@ -136,42 +131,11 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
136
131
|
selectedIndex={selectedRoleIndex}
|
|
137
132
|
flatRoles={flatRoles}
|
|
138
133
|
/>
|
|
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
134
|
</Box>
|
|
171
135
|
|
|
172
|
-
{/* Vertical separator —
|
|
136
|
+
{/* Vertical separator — memoized to avoid regenerating on every render */}
|
|
173
137
|
<Box flexDirection="column" marginX={0}>
|
|
174
|
-
<Text color="gray">{
|
|
138
|
+
<Text color="gray">{separatorStr}</Text>
|
|
175
139
|
</Box>
|
|
176
140
|
|
|
177
141
|
{/* Right: Agent Detail + Stream */}
|
package/src/tui/hooks/useApi.ts
CHANGED
package/src/tui/hooks/useSSE.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useSSE — subscribe to wave SSE stream with auto-reconnect
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* - msg:start task: truncate long supervisor prompts
|
|
7
|
-
* - tool inputs: summarize
|
|
4
|
+
* Performance: batches events and updates React state max once per 300ms
|
|
5
|
+
* to prevent fullscreen Panel Mode from re-rendering on every text chunk.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
@@ -13,12 +11,12 @@ import { subscribeToWaveStream, type SSEEvent, type SSEConnection } from '../api
|
|
|
13
11
|
const MAX_EVENTS = 150;
|
|
14
12
|
const RECONNECT_DELAY_MS = 3000;
|
|
15
13
|
const MAX_RECONNECT_DELAY_MS = 15000;
|
|
14
|
+
const BATCH_INTERVAL_MS = 300; // Throttle: update React state max ~3x/sec
|
|
16
15
|
|
|
17
16
|
/** Trim event data to prevent memory bloat */
|
|
18
17
|
function trimEvent(event: SSEEvent): SSEEvent {
|
|
19
18
|
const data = { ...event.data };
|
|
20
19
|
|
|
21
|
-
// Truncate large text fields
|
|
22
20
|
if (typeof data.text === 'string' && data.text.length > 500) {
|
|
23
21
|
data.text = data.text.slice(0, 500);
|
|
24
22
|
}
|
|
@@ -31,16 +29,11 @@ function trimEvent(event: SSEEvent): SSEEvent {
|
|
|
31
29
|
if (typeof data.message === 'string' && data.message.length > 300) {
|
|
32
30
|
data.message = data.message.slice(0, 300);
|
|
33
31
|
}
|
|
34
|
-
// Summarize tool inputs
|
|
35
32
|
if (data.input && typeof data.input === 'object') {
|
|
36
33
|
const inp = data.input as Record<string, unknown>;
|
|
37
34
|
const summary: Record<string, unknown> = {};
|
|
38
35
|
for (const [k, v] of Object.entries(inp)) {
|
|
39
|
-
|
|
40
|
-
summary[k] = v.slice(0, 200);
|
|
41
|
-
} else {
|
|
42
|
-
summary[k] = v;
|
|
43
|
-
}
|
|
36
|
+
summary[k] = typeof v === 'string' && v.length > 200 ? v.slice(0, 200) : v;
|
|
44
37
|
}
|
|
45
38
|
data.input = summary;
|
|
46
39
|
}
|
|
@@ -63,7 +56,24 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
63
56
|
const reconnectAttemptRef = useRef(0);
|
|
64
57
|
const maxSeqRef = useRef(0);
|
|
65
58
|
|
|
59
|
+
// Batch buffer — accumulate events, flush to React state periodically
|
|
60
|
+
const batchRef = useRef<SSEEvent[]>([]);
|
|
61
|
+
const batchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
62
|
+
|
|
63
|
+
const flushBatch = useCallback(() => {
|
|
64
|
+
batchTimerRef.current = null;
|
|
65
|
+
const batch = batchRef.current;
|
|
66
|
+
if (batch.length === 0) return;
|
|
67
|
+
batchRef.current = [];
|
|
68
|
+
|
|
69
|
+
setEvents((prev) => {
|
|
70
|
+
const next = [...prev, ...batch];
|
|
71
|
+
return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
|
|
72
|
+
});
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
66
75
|
const clearEvents = useCallback(() => {
|
|
76
|
+
batchRef.current = [];
|
|
67
77
|
setEvents([]);
|
|
68
78
|
maxSeqRef.current = 0;
|
|
69
79
|
}, []);
|
|
@@ -74,10 +84,15 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
74
84
|
connRef.current = null;
|
|
75
85
|
waveIdRef.current = waveId;
|
|
76
86
|
reconnectAttemptRef.current = 0;
|
|
87
|
+
batchRef.current = [];
|
|
77
88
|
if (reconnectTimerRef.current) {
|
|
78
89
|
clearTimeout(reconnectTimerRef.current);
|
|
79
90
|
reconnectTimerRef.current = null;
|
|
80
91
|
}
|
|
92
|
+
if (batchTimerRef.current) {
|
|
93
|
+
clearTimeout(batchTimerRef.current);
|
|
94
|
+
batchTimerRef.current = null;
|
|
95
|
+
}
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
if (!waveId) {
|
|
@@ -90,6 +105,7 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
90
105
|
|
|
91
106
|
if (fromSeq === 0) {
|
|
92
107
|
setEvents([]);
|
|
108
|
+
batchRef.current = [];
|
|
93
109
|
maxSeqRef.current = 0;
|
|
94
110
|
}
|
|
95
111
|
|
|
@@ -101,13 +117,18 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
101
117
|
}
|
|
102
118
|
reconnectAttemptRef.current = 0;
|
|
103
119
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
// Add to batch buffer (don't trigger React re-render yet)
|
|
121
|
+
batchRef.current.push(trimEvent(event));
|
|
122
|
+
|
|
123
|
+
// Schedule flush if not already scheduled
|
|
124
|
+
if (!batchTimerRef.current) {
|
|
125
|
+
batchTimerRef.current = setTimeout(flushBatch, BATCH_INTERVAL_MS);
|
|
126
|
+
}
|
|
109
127
|
},
|
|
110
128
|
(reason) => {
|
|
129
|
+
// Flush remaining events before status change
|
|
130
|
+
flushBatch();
|
|
131
|
+
|
|
111
132
|
if (reason === 'done') {
|
|
112
133
|
setStreamStatus('done');
|
|
113
134
|
} else {
|
|
@@ -140,8 +161,12 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
140
161
|
clearTimeout(reconnectTimerRef.current);
|
|
141
162
|
reconnectTimerRef.current = null;
|
|
142
163
|
}
|
|
164
|
+
if (batchTimerRef.current) {
|
|
165
|
+
clearTimeout(batchTimerRef.current);
|
|
166
|
+
batchTimerRef.current = null;
|
|
167
|
+
}
|
|
143
168
|
};
|
|
144
|
-
}, [waveId]);
|
|
169
|
+
}, [waveId, flushBatch]);
|
|
145
170
|
|
|
146
171
|
return { events, streamStatus, clearEvents };
|
|
147
172
|
}
|