tycono 0.1.96-beta.23 → 0.1.96-beta.25
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
|
@@ -194,14 +194,14 @@ export const App: React.FC = () => {
|
|
|
194
194
|
const addSystemMessage = useCallback((text: string, color: string = 'yellow') => {
|
|
195
195
|
setSystemMessages(prev => {
|
|
196
196
|
const next = [...prev, { id: ++sysLineId, text, color }];
|
|
197
|
-
return next.length >
|
|
197
|
+
return next.length > 30 ? next.slice(-30) : next;
|
|
198
198
|
});
|
|
199
199
|
}, []);
|
|
200
200
|
|
|
201
201
|
const addSystemLines = useCallback((lines: StreamLine[]) => {
|
|
202
202
|
setSystemMessages(prev => {
|
|
203
203
|
const next = [...prev, ...lines];
|
|
204
|
-
return next.length >
|
|
204
|
+
return next.length > 40 ? next.slice(-40) : next;
|
|
205
205
|
});
|
|
206
206
|
}, []);
|
|
207
207
|
|
|
@@ -261,8 +261,8 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
261
261
|
if (line) eventLines.push(line);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
// Merge system messages and event lines
|
|
265
|
-
const allLines = [...systemMessages, ...eventLines];
|
|
264
|
+
// Merge system messages and event lines (cap total to prevent memory bloat)
|
|
265
|
+
const allLines = [...systemMessages, ...eventLines].slice(-200);
|
|
266
266
|
|
|
267
267
|
// Split into committed (scrollback) and live (re-rendered)
|
|
268
268
|
const newCommitted = allLines.slice(committedRef.current);
|
|
@@ -271,7 +271,9 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
271
271
|
committedRef.current += toCommit.length;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
// Cap committed to prevent Static from holding too many items
|
|
275
|
+
const rawCommitted = allLines.slice(0, committedRef.current);
|
|
276
|
+
const committedLines = rawCommitted.length > 100 ? rawCommitted.slice(-100) : rawCommitted;
|
|
275
277
|
const liveLines = allLines.slice(committedRef.current);
|
|
276
278
|
|
|
277
279
|
const handleSubmit = useCallback((value: string) => {
|
|
@@ -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();
|
|
@@ -169,9 +172,9 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
169
172
|
</Box>
|
|
170
173
|
</Box>
|
|
171
174
|
|
|
172
|
-
{/* Vertical separator —
|
|
175
|
+
{/* Vertical separator — memoized to avoid regenerating on every render */}
|
|
173
176
|
<Box flexDirection="column" marginX={0}>
|
|
174
|
-
<Text color="gray">{
|
|
177
|
+
<Text color="gray">{separatorStr}</Text>
|
|
175
178
|
</Box>
|
|
176
179
|
|
|
177
180
|
{/* Right: Agent Detail + Stream */}
|
package/src/tui/hooks/useApi.ts
CHANGED
package/src/tui/hooks/useSSE.ts
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useSSE — subscribe to wave SSE stream with auto-reconnect
|
|
3
|
+
*
|
|
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.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
6
9
|
import { subscribeToWaveStream, type SSEEvent, type SSEConnection } from '../api';
|
|
7
10
|
|
|
8
|
-
const MAX_EVENTS =
|
|
11
|
+
const MAX_EVENTS = 150;
|
|
9
12
|
const RECONNECT_DELAY_MS = 3000;
|
|
10
13
|
const MAX_RECONNECT_DELAY_MS = 15000;
|
|
14
|
+
const BATCH_INTERVAL_MS = 300; // Throttle: update React state max ~3x/sec
|
|
15
|
+
|
|
16
|
+
/** Trim event data to prevent memory bloat */
|
|
17
|
+
function trimEvent(event: SSEEvent): SSEEvent {
|
|
18
|
+
const data = { ...event.data };
|
|
19
|
+
|
|
20
|
+
if (typeof data.text === 'string' && data.text.length > 500) {
|
|
21
|
+
data.text = data.text.slice(0, 500);
|
|
22
|
+
}
|
|
23
|
+
if (typeof data.task === 'string' && data.task.length > 200) {
|
|
24
|
+
data.task = data.task.slice(0, 200);
|
|
25
|
+
}
|
|
26
|
+
if (typeof data.error === 'string' && data.error.length > 300) {
|
|
27
|
+
data.error = data.error.slice(0, 300);
|
|
28
|
+
}
|
|
29
|
+
if (typeof data.message === 'string' && data.message.length > 300) {
|
|
30
|
+
data.message = data.message.slice(0, 300);
|
|
31
|
+
}
|
|
32
|
+
if (data.input && typeof data.input === 'object') {
|
|
33
|
+
const inp = data.input as Record<string, unknown>;
|
|
34
|
+
const summary: Record<string, unknown> = {};
|
|
35
|
+
for (const [k, v] of Object.entries(inp)) {
|
|
36
|
+
summary[k] = typeof v === 'string' && v.length > 200 ? v.slice(0, 200) : v;
|
|
37
|
+
}
|
|
38
|
+
data.input = summary;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { ...event, data };
|
|
42
|
+
}
|
|
11
43
|
|
|
12
44
|
export interface SSEState {
|
|
13
45
|
events: SSEEvent[];
|
|
@@ -24,22 +56,43 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
24
56
|
const reconnectAttemptRef = useRef(0);
|
|
25
57
|
const maxSeqRef = useRef(0);
|
|
26
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
|
+
|
|
27
75
|
const clearEvents = useCallback(() => {
|
|
76
|
+
batchRef.current = [];
|
|
28
77
|
setEvents([]);
|
|
29
78
|
maxSeqRef.current = 0;
|
|
30
79
|
}, []);
|
|
31
80
|
|
|
32
81
|
useEffect(() => {
|
|
33
|
-
// Disconnect if waveId changed
|
|
34
82
|
if (waveIdRef.current !== waveId) {
|
|
35
83
|
connRef.current?.close();
|
|
36
84
|
connRef.current = null;
|
|
37
85
|
waveIdRef.current = waveId;
|
|
38
86
|
reconnectAttemptRef.current = 0;
|
|
87
|
+
batchRef.current = [];
|
|
39
88
|
if (reconnectTimerRef.current) {
|
|
40
89
|
clearTimeout(reconnectTimerRef.current);
|
|
41
90
|
reconnectTimerRef.current = null;
|
|
42
91
|
}
|
|
92
|
+
if (batchTimerRef.current) {
|
|
93
|
+
clearTimeout(batchTimerRef.current);
|
|
94
|
+
batchTimerRef.current = null;
|
|
95
|
+
}
|
|
43
96
|
}
|
|
44
97
|
|
|
45
98
|
if (!waveId) {
|
|
@@ -50,39 +103,41 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
50
103
|
const connect = (fromSeq: number) => {
|
|
51
104
|
setStreamStatus('streaming');
|
|
52
105
|
|
|
53
|
-
// Only clear events on first connect (not reconnects)
|
|
54
106
|
if (fromSeq === 0) {
|
|
55
107
|
setEvents([]);
|
|
108
|
+
batchRef.current = [];
|
|
56
109
|
maxSeqRef.current = 0;
|
|
57
110
|
}
|
|
58
111
|
|
|
59
112
|
const conn = subscribeToWaveStream(
|
|
60
113
|
waveId,
|
|
61
114
|
(event) => {
|
|
62
|
-
// Track max seq for reconnect resume
|
|
63
115
|
if (event.seq !== undefined && event.seq > maxSeqRef.current) {
|
|
64
116
|
maxSeqRef.current = event.seq;
|
|
65
117
|
}
|
|
66
|
-
reconnectAttemptRef.current = 0;
|
|
118
|
+
reconnectAttemptRef.current = 0;
|
|
119
|
+
|
|
120
|
+
// Add to batch buffer (don't trigger React re-render yet)
|
|
121
|
+
batchRef.current.push(trimEvent(event));
|
|
67
122
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
123
|
+
// Schedule flush if not already scheduled
|
|
124
|
+
if (!batchTimerRef.current) {
|
|
125
|
+
batchTimerRef.current = setTimeout(flushBatch, BATCH_INTERVAL_MS);
|
|
126
|
+
}
|
|
72
127
|
},
|
|
73
128
|
(reason) => {
|
|
129
|
+
// Flush remaining events before status change
|
|
130
|
+
flushBatch();
|
|
131
|
+
|
|
74
132
|
if (reason === 'done') {
|
|
75
133
|
setStreamStatus('done');
|
|
76
134
|
} else {
|
|
77
|
-
// Disconnected or error → auto-reconnect
|
|
78
135
|
const attempt = reconnectAttemptRef.current++;
|
|
79
136
|
const delay = Math.min(
|
|
80
137
|
RECONNECT_DELAY_MS * Math.pow(1.5, attempt),
|
|
81
138
|
MAX_RECONNECT_DELAY_MS,
|
|
82
139
|
);
|
|
83
|
-
|
|
84
|
-
setStreamStatus('streaming'); // Keep showing streaming during reconnect
|
|
85
|
-
|
|
140
|
+
setStreamStatus('streaming');
|
|
86
141
|
reconnectTimerRef.current = setTimeout(() => {
|
|
87
142
|
reconnectTimerRef.current = null;
|
|
88
143
|
if (waveIdRef.current === waveId) {
|
|
@@ -106,8 +161,12 @@ export function useSSE(waveId: string | null): SSEState {
|
|
|
106
161
|
clearTimeout(reconnectTimerRef.current);
|
|
107
162
|
reconnectTimerRef.current = null;
|
|
108
163
|
}
|
|
164
|
+
if (batchTimerRef.current) {
|
|
165
|
+
clearTimeout(batchTimerRef.current);
|
|
166
|
+
batchTimerRef.current = null;
|
|
167
|
+
}
|
|
109
168
|
};
|
|
110
|
-
}, [waveId]);
|
|
169
|
+
}, [waveId, flushBatch]);
|
|
111
170
|
|
|
112
171
|
return { events, streamStatus, clearEvents };
|
|
113
172
|
}
|