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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.96-beta.23",
3
+ "version": "0.1.96-beta.25",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
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 > 50 ? next.slice(-50) : next;
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 > 80 ? next.slice(-80) : next;
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
- const committedLines = allLines.slice(0, committedRef.current);
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 — fill available height */}
175
+ {/* Vertical separator — memoized to avoid regenerating on every render */}
173
176
  <Box flexDirection="column" marginX={0}>
174
- <Text color="gray">{'\u2502\n'.repeat(Math.max(5, termHeight - 6))}</Text>
177
+ <Text color="gray">{separatorStr}</Text>
175
178
  </Box>
176
179
 
177
180
  {/* Right: Agent Detail + Stream */}
@@ -15,7 +15,7 @@ import {
15
15
  type ActiveSessionInfo,
16
16
  } from '../api';
17
17
 
18
- const POLL_INTERVAL = 3000; // 3 seconds
18
+ const POLL_INTERVAL = 5000; // 5 seconds (reduce re-renders)
19
19
 
20
20
  export interface ActiveWaveInfo {
21
21
  waveId: string;
@@ -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 = 500;
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; // Reset on successful event
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
- setEvents((prev) => {
69
- const next = [...prev, event];
70
- return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
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
  }