tycono 0.1.96-beta.22 → 0.1.96-beta.24

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.22",
3
+ "version": "0.1.96-beta.24",
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
 
@@ -2,8 +2,7 @@
2
2
  * CommandMode — scrollable terminal mode (like Claude Code)
3
3
  *
4
4
  * Uses Ink's <Static> to push past output into terminal scrollback.
5
- * Only the input prompt + status remain in the re-rendered area.
6
- * User can scroll up with mouse wheel to see history.
5
+ * Shows full output: text, tools, thinking, dispatch no aggressive truncation.
7
6
  */
8
7
 
9
8
  import React, { useState, useCallback, useRef } from 'react';
@@ -32,17 +31,13 @@ interface CommandModeProps {
32
31
 
33
32
  let lineCounter = 0;
34
33
 
35
- /** Filter out system prompt noise from text */
34
+ /** Filter out only truly internal system prompt fragments */
36
35
  function isSystemNoise(text: string): boolean {
37
36
  const t = text.trim();
38
37
  if (!t) return true;
39
- if (t.startsWith('## Your Role')) return true;
40
- if (t.startsWith('You are')) return true;
41
- if (t.startsWith('[CEO Supervisor]')) return true;
42
- if (t.startsWith('[Question from')) return true;
43
- if (t.includes('\u26D4 AKB Rule')) return true;
44
- if (t.includes('\u26D4 Read the')) return true;
45
- if (t.startsWith('\u26D4')) return true;
38
+ // Only filter the injected supervisor system prompt header
39
+ if (t.startsWith('[CEO Supervisor]') && t.includes('Your Role')) return true;
40
+ if (t.startsWith('\u26D4 AKB Rule:')) return true;
46
41
  return false;
47
42
  }
48
43
 
@@ -59,7 +54,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
59
54
  if (isSupervisor) {
60
55
  return {
61
56
  id: ++lineCounter,
62
- text: text.slice(0, 200),
57
+ text,
63
58
  color: 'white',
64
59
  };
65
60
  } else {
@@ -67,17 +62,30 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
67
62
  id: ++lineCounter,
68
63
  prefix: event.roleId,
69
64
  prefixColor: roleColor,
70
- text: text.slice(0, 80),
65
+ text,
71
66
  color: 'white',
72
67
  indent: true,
73
68
  };
74
69
  }
75
70
  }
76
71
 
72
+ case 'thinking': {
73
+ const text = ((event.data.text as string) ?? '').slice(0, 120);
74
+ if (!text.trim()) return null;
75
+ return {
76
+ id: ++lineCounter,
77
+ prefix: isSupervisor ? undefined : event.roleId,
78
+ prefixColor: roleColor,
79
+ text: `\uD83D\uDCAD ${text}`,
80
+ color: 'gray',
81
+ indent: !isSupervisor,
82
+ };
83
+ }
84
+
77
85
  case 'dispatch:start': {
78
86
  const target = (event.data.targetRole as string) ?? '';
79
87
  const task = ((event.data.task as string) ?? '');
80
- const cleanTask = task.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 50);
88
+ const cleanTask = task.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 80);
81
89
  if (isSupervisor) {
82
90
  return {
83
91
  id: ++lineCounter,
@@ -89,7 +97,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
89
97
  id: ++lineCounter,
90
98
  prefix: event.roleId,
91
99
  prefixColor: roleColor,
92
- text: `\u2192 ${target} \uBC30\uC815`,
100
+ text: `\u2192 ${target} \uBC30\uC815${cleanTask ? ': ' + cleanTask : ''}`,
93
101
  color: 'yellow',
94
102
  indent: true,
95
103
  };
@@ -99,7 +107,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
99
107
  const target = (event.data.targetRole as string) ?? '';
100
108
  return {
101
109
  id: ++lineCounter,
102
- prefix: event.roleId,
110
+ prefix: isSupervisor ? undefined : event.roleId,
103
111
  prefixColor: roleColor,
104
112
  text: `\u2190 ${target} \uC644\uB8CC`,
105
113
  color: 'yellow',
@@ -113,31 +121,43 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
113
121
  let detail = '';
114
122
  if (input && typeof input === 'object') {
115
123
  const inp = input as Record<string, unknown>;
116
- if (inp.file_path) detail = ` ${String(inp.file_path).split('/').pop()}`;
117
- else if (inp.command) detail = ` ${String(inp.command).slice(0, 40)}`;
124
+ if (inp.file_path) detail = ` ${String(inp.file_path)}`;
125
+ else if (inp.command) detail = ` ${String(inp.command).slice(0, 80)}`;
126
+ else if (inp.pattern) detail = ` ${String(inp.pattern)}`;
127
+ else if (inp.description) detail = ` ${String(inp.description).slice(0, 60)}`;
118
128
  }
129
+ return {
130
+ id: ++lineCounter,
131
+ prefix: isSupervisor ? undefined : event.roleId,
132
+ prefixColor: roleColor,
133
+ text: ` \u2192 ${toolName}${detail}`,
134
+ color: 'gray',
135
+ indent: !isSupervisor,
136
+ };
137
+ }
119
138
 
120
- if (isSupervisor) {
121
- return {
122
- id: ++lineCounter,
123
- text: ` \u2192 ${toolName}${detail}`,
124
- color: 'gray',
125
- };
126
- }
139
+ case 'tool:result': {
140
+ const toolName = (event.data.name as string) ?? 'tool';
127
141
  return {
128
142
  id: ++lineCounter,
129
- prefix: event.roleId,
143
+ prefix: isSupervisor ? undefined : event.roleId,
130
144
  prefixColor: roleColor,
131
- text: `\u2192 ${toolName}${detail}`,
145
+ text: ` \u2190 ${toolName} done`,
132
146
  color: 'gray',
133
- indent: true,
147
+ indent: !isSupervisor,
134
148
  };
135
149
  }
136
150
 
137
151
  case 'msg:start': {
138
- if (isSupervisor) return null;
139
152
  const task = ((event.data.task as string) ?? '');
140
- const cleanTask = task.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 40);
153
+ const cleanTask = task.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 60);
154
+ if (isSupervisor) {
155
+ return {
156
+ id: ++lineCounter,
157
+ text: `\u25B6 Supervisor started${cleanTask ? ': ' + cleanTask : ''}`,
158
+ color: 'cyan',
159
+ };
160
+ }
141
161
  return {
142
162
  id: ++lineCounter,
143
163
  prefix: event.roleId,
@@ -150,7 +170,13 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
150
170
 
151
171
  case 'msg:done': {
152
172
  const turns = event.data.turns as number | undefined;
153
- if (isSupervisor) return null;
173
+ if (isSupervisor) {
174
+ return {
175
+ id: ++lineCounter,
176
+ text: `\u2713 Supervisor done${turns ? ` (${turns} turns)` : ''}`,
177
+ color: 'cyan',
178
+ };
179
+ }
154
180
  return {
155
181
  id: ++lineCounter,
156
182
  prefix: event.roleId,
@@ -162,8 +188,14 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
162
188
  }
163
189
 
164
190
  case 'msg:error': {
165
- if (isSupervisor) return null;
166
- const error = ((event.data.error as string) ?? '').slice(0, 60);
191
+ const error = ((event.data.error as string) ?? (event.data.message as string) ?? '').slice(0, 120);
192
+ if (isSupervisor) {
193
+ return {
194
+ id: ++lineCounter,
195
+ text: `\u2717 Supervisor error: ${error}`,
196
+ color: 'red',
197
+ };
198
+ }
167
199
  return {
168
200
  id: ++lineCounter,
169
201
  prefix: event.roleId,
@@ -174,20 +206,23 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
174
206
  };
175
207
  }
176
208
 
177
- case 'msg:awaiting_input':
209
+ case 'msg:awaiting_input': {
210
+ const question = (event.data.question as string) ?? '';
178
211
  return {
179
212
  id: ++lineCounter,
180
- text: isSupervisor ? '...' : ` ${event.roleId}: waiting`,
213
+ prefix: isSupervisor ? undefined : event.roleId,
214
+ prefixColor: roleColor,
215
+ text: question ? `? ${question.slice(0, 100)}` : '? Awaiting input...',
181
216
  color: 'yellow',
217
+ indent: !isSupervisor,
182
218
  };
219
+ }
183
220
 
184
- // Hidden
185
- case 'thinking':
221
+ // Hidden (truly internal)
186
222
  case 'heartbeat:tick':
187
223
  case 'heartbeat:skip':
188
224
  case 'prompt:assembled':
189
225
  case 'trace:response':
190
- case 'tool:result':
191
226
  return null;
192
227
 
193
228
  default:
@@ -205,7 +240,7 @@ function StreamLineRow({ line }: { line: StreamLine }) {
205
240
  {(line.prefix).padEnd(12)}
206
241
  </Text>
207
242
  )}
208
- <Text color={line.color}>{line.text}</Text>
243
+ <Text color={line.color} wrap="wrap">{line.text}</Text>
209
244
  </Box>
210
245
  );
211
246
  }
@@ -226,19 +261,19 @@ export const CommandMode: React.FC<CommandModeProps> = ({
226
261
  if (line) eventLines.push(line);
227
262
  }
228
263
 
229
- // Merge system messages and event lines
230
- const allLines = [...systemMessages, ...eventLines];
264
+ // Merge system messages and event lines (cap total to prevent memory bloat)
265
+ const allLines = [...systemMessages, ...eventLines].slice(-200);
231
266
 
232
267
  // Split into committed (scrollback) and live (re-rendered)
233
- // Lines up to committedRef are frozen in scrollback
234
268
  const newCommitted = allLines.slice(committedRef.current);
235
- if (newCommitted.length > 5) {
236
- // Keep last 5 lines live, push rest to scrollback
237
- const toCommit = newCommitted.slice(0, -5);
269
+ if (newCommitted.length > 8) {
270
+ const toCommit = newCommitted.slice(0, -8);
238
271
  committedRef.current += toCommit.length;
239
272
  }
240
273
 
241
- 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;
242
277
  const liveLines = allLines.slice(committedRef.current);
243
278
 
244
279
  const handleSubmit = useCallback((value: string) => {
@@ -251,12 +286,12 @@ export const CommandMode: React.FC<CommandModeProps> = ({
251
286
 
252
287
  return (
253
288
  <Box flexDirection="column">
254
- {/* Committed lines → pushed to terminal scrollback (scrollable with mouse wheel) */}
289
+ {/* Committed lines → pushed to terminal scrollback */}
255
290
  <Static items={committedLines}>
256
291
  {(line) => <StreamLineRow key={line.id} line={line} />}
257
292
  </Static>
258
293
 
259
- {/* Live lines → re-rendered on each update */}
294
+ {/* Live lines → re-rendered */}
260
295
  {liveLines.map((line) => (
261
296
  <StreamLineRow key={line.id} line={line} />
262
297
  ))}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * StreamView — detailed stream panel for Panel Mode (right side)
3
3
  * Shows full event details with timestamps for a selected role.
4
- * Reuses the rendering logic from StreamPanel v1 but with the v2 layout.
4
+ * No aggressive truncation shows tools, thinking, dispatch like Claude Code.
5
5
  */
6
6
 
7
7
  import React from 'react';
@@ -26,11 +26,11 @@ function formatTime(ts: string): string {
26
26
  }
27
27
  }
28
28
 
29
- function renderEvent(event: SSEEvent, allRoleIds: string[]): { content: string; contentColor: string } | null {
29
+ function renderEvent(event: SSEEvent): { content: string; contentColor: string } | null {
30
30
  switch (event.type) {
31
31
  case 'msg:start':
32
32
  return {
33
- content: `\u25B6 Started: ${(event.data.task as string)?.slice(0, 60) ?? ''}`,
33
+ content: `\u25B6 Started: ${(event.data.task as string)?.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 80) ?? ''}`,
34
34
  contentColor: 'green',
35
35
  };
36
36
 
@@ -44,16 +44,23 @@ function renderEvent(event: SSEEvent, allRoleIds: string[]): { content: string;
44
44
 
45
45
  case 'msg:error':
46
46
  return {
47
- content: `\u2717 Error: ${(event.data.error as string)?.slice(0, 60) ?? ''}`,
47
+ content: `\u2717 Error: ${(event.data.error as string ?? event.data.message as string ?? '').slice(0, 120)}`,
48
48
  contentColor: 'red',
49
49
  };
50
50
 
51
51
  case 'text': {
52
- const text = ((event.data.text as string) ?? '').slice(0, 120);
52
+ const text = ((event.data.text as string) ?? '');
53
53
  if (!text.trim()) return null;
54
+ // Don't truncate — let terminal wrap
54
55
  return { content: text, contentColor: 'white' };
55
56
  }
56
57
 
58
+ case 'thinking': {
59
+ const text = ((event.data.text as string) ?? '').slice(0, 150);
60
+ if (!text.trim()) return null;
61
+ return { content: `\uD83D\uDCAD ${text}`, contentColor: 'gray' };
62
+ }
63
+
57
64
  case 'tool:start': {
58
65
  const name = (event.data.name as string) ?? 'tool';
59
66
  const input = event.data.input;
@@ -61,8 +68,9 @@ function renderEvent(event: SSEEvent, allRoleIds: string[]): { content: string;
61
68
  if (input && typeof input === 'object') {
62
69
  const inp = input as Record<string, unknown>;
63
70
  if (inp.file_path) detail = ` ${String(inp.file_path)}`;
64
- else if (inp.command) detail = ` ${String(inp.command).slice(0, 60)}`;
65
- else detail = ` ${JSON.stringify(input).slice(0, 60)}`;
71
+ else if (inp.command) detail = ` ${String(inp.command).slice(0, 80)}`;
72
+ else if (inp.pattern) detail = ` ${String(inp.pattern)}`;
73
+ else detail = ` ${JSON.stringify(input).slice(0, 80)}`;
66
74
  }
67
75
  return {
68
76
  content: `\u2192 ${name}${detail}`,
@@ -78,7 +86,7 @@ function renderEvent(event: SSEEvent, allRoleIds: string[]): { content: string;
78
86
 
79
87
  case 'dispatch:start':
80
88
  return {
81
- content: `\u21D2 dispatch ${event.data.targetRole as string ?? ''}: ${(event.data.task as string)?.slice(0, 50) ?? ''}`,
89
+ content: `\u21D2 dispatch ${event.data.targetRole as string ?? ''}: ${(event.data.task as string)?.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 80) ?? ''}`,
82
90
  contentColor: 'yellow',
83
91
  };
84
92
 
@@ -88,14 +96,15 @@ function renderEvent(event: SSEEvent, allRoleIds: string[]): { content: string;
88
96
  contentColor: 'yellow',
89
97
  };
90
98
 
91
- case 'msg:awaiting_input':
99
+ case 'msg:awaiting_input': {
100
+ const question = (event.data.question as string) ?? '';
92
101
  return {
93
- content: '? Awaiting input...',
102
+ content: question ? `? ${question.slice(0, 120)}` : '? Awaiting input...',
94
103
  contentColor: 'yellow',
95
104
  };
105
+ }
96
106
 
97
- // Hidden events
98
- case 'thinking':
107
+ // Hidden (truly internal only)
99
108
  case 'heartbeat:tick':
100
109
  case 'heartbeat:skip':
101
110
  case 'prompt:assembled':
@@ -114,7 +123,7 @@ export const StreamView: React.FC<StreamViewProps> = ({
114
123
  waveId,
115
124
  roleLabel,
116
125
  }) => {
117
- const maxVisible = 20;
126
+ const maxVisible = 30;
118
127
  const visibleEvents = events.slice(-maxVisible);
119
128
 
120
129
  const turnCount = events.filter(e => e.type === 'text' || e.type === 'tool:start').length;
@@ -146,14 +155,14 @@ export const StreamView: React.FC<StreamViewProps> = ({
146
155
  )}
147
156
 
148
157
  {visibleEvents.map((event, i) => {
149
- const rendered = renderEvent(event, allRoleIds);
158
+ const rendered = renderEvent(event);
150
159
  if (!rendered) return null;
151
160
  const roleColor = getRoleColor(event.roleId, allRoleIds);
152
161
  return (
153
162
  <Box key={`${event.seq}-${i}`}>
154
163
  <Text color="gray" dimColor>{formatTime(event.ts)} </Text>
155
164
  <Text color={roleColor} bold>{event.roleId.padEnd(12)}</Text>
156
- <Text color={rendered.contentColor}>{rendered.content}</Text>
165
+ <Text color={rendered.contentColor} wrap="wrap">{rendered.content}</Text>
157
166
  </Box>
158
167
  );
159
168
  })}
@@ -1,14 +1,53 @@
1
1
  /**
2
2
  * useSSE — subscribe to wave SSE stream with auto-reconnect
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
3
8
  */
4
9
 
5
10
  import { useState, useEffect, useRef, useCallback } from 'react';
6
11
  import { subscribeToWaveStream, type SSEEvent, type SSEConnection } from '../api';
7
12
 
8
- const MAX_EVENTS = 500;
13
+ const MAX_EVENTS = 150;
9
14
  const RECONNECT_DELAY_MS = 3000;
10
15
  const MAX_RECONNECT_DELAY_MS = 15000;
11
16
 
17
+ /** Trim event data to prevent memory bloat */
18
+ function trimEvent(event: SSEEvent): SSEEvent {
19
+ const data = { ...event.data };
20
+
21
+ // Truncate large text fields
22
+ if (typeof data.text === 'string' && data.text.length > 500) {
23
+ data.text = data.text.slice(0, 500);
24
+ }
25
+ if (typeof data.task === 'string' && data.task.length > 200) {
26
+ data.task = data.task.slice(0, 200);
27
+ }
28
+ if (typeof data.error === 'string' && data.error.length > 300) {
29
+ data.error = data.error.slice(0, 300);
30
+ }
31
+ if (typeof data.message === 'string' && data.message.length > 300) {
32
+ data.message = data.message.slice(0, 300);
33
+ }
34
+ // Summarize tool inputs
35
+ if (data.input && typeof data.input === 'object') {
36
+ const inp = data.input as Record<string, unknown>;
37
+ const summary: Record<string, unknown> = {};
38
+ 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
+ }
44
+ }
45
+ data.input = summary;
46
+ }
47
+
48
+ return { ...event, data };
49
+ }
50
+
12
51
  export interface SSEState {
13
52
  events: SSEEvent[];
14
53
  streamStatus: 'idle' | 'streaming' | 'done' | 'error';
@@ -30,7 +69,6 @@ export function useSSE(waveId: string | null): SSEState {
30
69
  }, []);
31
70
 
32
71
  useEffect(() => {
33
- // Disconnect if waveId changed
34
72
  if (waveIdRef.current !== waveId) {
35
73
  connRef.current?.close();
36
74
  connRef.current = null;
@@ -50,7 +88,6 @@ export function useSSE(waveId: string | null): SSEState {
50
88
  const connect = (fromSeq: number) => {
51
89
  setStreamStatus('streaming');
52
90
 
53
- // Only clear events on first connect (not reconnects)
54
91
  if (fromSeq === 0) {
55
92
  setEvents([]);
56
93
  maxSeqRef.current = 0;
@@ -59,14 +96,14 @@ export function useSSE(waveId: string | null): SSEState {
59
96
  const conn = subscribeToWaveStream(
60
97
  waveId,
61
98
  (event) => {
62
- // Track max seq for reconnect resume
63
99
  if (event.seq !== undefined && event.seq > maxSeqRef.current) {
64
100
  maxSeqRef.current = event.seq;
65
101
  }
66
- reconnectAttemptRef.current = 0; // Reset on successful event
102
+ reconnectAttemptRef.current = 0;
67
103
 
104
+ const trimmed = trimEvent(event);
68
105
  setEvents((prev) => {
69
- const next = [...prev, event];
106
+ const next = [...prev, trimmed];
70
107
  return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
71
108
  });
72
109
  },
@@ -74,15 +111,12 @@ export function useSSE(waveId: string | null): SSEState {
74
111
  if (reason === 'done') {
75
112
  setStreamStatus('done');
76
113
  } else {
77
- // Disconnected or error → auto-reconnect
78
114
  const attempt = reconnectAttemptRef.current++;
79
115
  const delay = Math.min(
80
116
  RECONNECT_DELAY_MS * Math.pow(1.5, attempt),
81
117
  MAX_RECONNECT_DELAY_MS,
82
118
  );
83
-
84
- setStreamStatus('streaming'); // Keep showing streaming during reconnect
85
-
119
+ setStreamStatus('streaming');
86
120
  reconnectTimerRef.current = setTimeout(() => {
87
121
  reconnectTimerRef.current = null;
88
122
  if (waveIdRef.current === waveId) {