tycono 0.1.96-beta.24 → 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.24",
3
+ "version": "0.1.96-beta.25",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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,10 +1,8 @@
1
1
  /**
2
2
  * useSSE — subscribe to wave SSE stream with auto-reconnect
3
3
  *
4
- * Events are stored with trimmed data to prevent OOM:
5
- * - text/thinking: keep only displayable portion
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
- if (typeof v === 'string' && v.length > 200) {
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
- const trimmed = trimEvent(event);
105
- setEvents((prev) => {
106
- const next = [...prev, trimmed];
107
- return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
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
  }