tycono 0.1.96-beta.21 → 0.1.96-beta.23

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.21",
3
+ "version": "0.1.96-beta.23",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/app.tsx CHANGED
@@ -407,47 +407,60 @@ export const App: React.FC = () => {
407
407
  );
408
408
  }
409
409
 
410
- return (
411
- <Box flexDirection="column" height={termHeight}>
412
- {/* Mode content fill remaining height */}
413
- <Box flexGrow={1} flexDirection="column">
414
- {mode === 'command' ? (
415
- <CommandMode
416
- events={sse.events}
417
- allRoleIds={flatRoleIds}
418
- systemMessages={systemMessages}
419
- onSubmit={handleCommandSubmit}
420
- />
421
- ) : (
422
- <PanelMode
423
- tree={orgTree}
424
- flatRoles={flatRoleIds}
425
- events={sse.events}
426
- selectedRoleIndex={selectedRoleIndex}
427
- selectedRoleId={selectedRoleId}
428
- streamStatus={sse.streamStatus}
429
- waveId={focusedWaveId}
430
- activeSessions={api.activeSessions}
431
- waves={waves}
432
- focusedWaveId={focusedWaveId}
433
- portSummary={api.portSummary}
434
- onMove={(dir) => {
435
- if (dir === 'up') {
436
- setSelectedRoleIndex(Math.max(0, selectedRoleIndex - 1));
437
- } else {
438
- setSelectedRoleIndex(Math.min(flatRoleIds.length - 1, selectedRoleIndex + 1));
439
- }
440
- }}
441
- onSelect={() => {
442
- const roleId = flatRoleIds[selectedRoleIndex] ?? null;
443
- setSelectedRoleId(roleId === selectedRoleId ? null : roleId);
444
- }}
445
- onEscape={() => setMode('command')}
410
+ // Command Mode: scrollable terminal (no fullscreen)
411
+ // Panel Mode: fullscreen (intentional — like vim for inspection)
412
+ if (mode === 'panel') {
413
+ return (
414
+ <Box flexDirection="column" height={termHeight}>
415
+ <Box flexGrow={1} flexDirection="column">
416
+ <PanelMode
417
+ tree={orgTree}
418
+ flatRoles={flatRoleIds}
419
+ events={sse.events}
420
+ selectedRoleIndex={selectedRoleIndex}
421
+ selectedRoleId={selectedRoleId}
422
+ streamStatus={sse.streamStatus}
423
+ waveId={focusedWaveId}
424
+ activeSessions={api.activeSessions}
425
+ waves={waves}
426
+ focusedWaveId={focusedWaveId}
427
+ portSummary={api.portSummary}
428
+ onMove={(dir) => {
429
+ if (dir === 'up') {
430
+ setSelectedRoleIndex(Math.max(0, selectedRoleIndex - 1));
431
+ } else {
432
+ setSelectedRoleIndex(Math.min(flatRoleIds.length - 1, selectedRoleIndex + 1));
433
+ }
434
+ }}
435
+ onSelect={() => {
436
+ const roleId = flatRoleIds[selectedRoleIndex] ?? null;
437
+ setSelectedRoleId(roleId === selectedRoleId ? null : roleId);
438
+ }}
439
+ onEscape={() => setMode('command')}
440
+ />
441
+ </Box>
442
+ <StatusBar
443
+ companyName={api.company?.name ?? 'Loading...'}
444
+ waveIndex={focusedWaveIndex}
445
+ waveCount={waves.length}
446
+ waveStatus={derivedWaveStatus}
447
+ activeCount={activeCount}
448
+ portCount={api.portSummary.totalPorts}
449
+ totalCost={0}
446
450
  />
447
- )}
448
451
  </Box>
452
+ );
453
+ }
449
454
 
450
- {/* Status Bar bottom (Claude Code style) */}
455
+ // Command Mode: natural terminal flow (scrollable with mouse wheel)
456
+ return (
457
+ <Box flexDirection="column">
458
+ <CommandMode
459
+ events={sse.events}
460
+ allRoleIds={flatRoleIds}
461
+ systemMessages={systemMessages}
462
+ onSubmit={handleCommandSubmit}
463
+ />
451
464
  <StatusBar
452
465
  companyName={api.company?.name ?? 'Loading...'}
453
466
  waveIndex={focusedWaveIndex}
@@ -1,19 +1,16 @@
1
1
  /**
2
- * CommandMode — chat-first mode
2
+ * CommandMode — scrollable terminal mode (like Claude Code)
3
3
  *
4
- * User = CEO. Supervisor (ceo role) = user's AI proxy.
5
- * - Supervisor responses: shown directly (no prefix), like a conversation
6
- * - Team activity: indented with roleId, concise
7
- * - System prompts, internal noise: filtered out
4
+ * Uses Ink's <Static> to push past output into terminal scrollback.
5
+ * Shows full output: text, tools, thinking, dispatch no aggressive truncation.
8
6
  */
9
7
 
10
- import React, { useState, useCallback } from 'react';
11
- import { Box, Text } from 'ink';
8
+ import React, { useState, useCallback, useRef } from 'react';
9
+ import { Box, Text, Static } from 'ink';
12
10
  import TextInput from 'ink-text-input';
13
11
  import type { SSEEvent } from '../api';
14
12
  import { getRoleColor } from '../theme';
15
13
 
16
- const MAX_STREAM_LINES = 30;
17
14
  const SUPERVISOR_ROLE = 'ceo';
18
15
 
19
16
  export interface StreamLine {
@@ -34,18 +31,13 @@ interface CommandModeProps {
34
31
 
35
32
  let lineCounter = 0;
36
33
 
37
- /** Filter out system prompt noise from text */
34
+ /** Filter out only truly internal system prompt fragments */
38
35
  function isSystemNoise(text: string): boolean {
39
36
  const t = text.trim();
40
37
  if (!t) return true;
41
- // System prompt fragments
42
- if (t.startsWith('## Your Role')) return true;
43
- if (t.startsWith('You are')) return true;
44
- if (t.startsWith('[CEO Supervisor]')) return true;
45
- if (t.startsWith('[Question from')) return true;
46
- if (t.includes('⛔ AKB Rule')) return true;
47
- if (t.includes('⛔ Read the')) return true;
48
- if (t.startsWith('⛔')) 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;
49
41
  return false;
50
42
  }
51
43
 
@@ -60,34 +52,44 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
60
52
  if (isSystemNoise(text)) return null;
61
53
 
62
54
  if (isSupervisor) {
63
- // Supervisor text → direct response (no prefix, generous length)
64
55
  return {
65
56
  id: ++lineCounter,
66
- text: text.slice(0, 200),
57
+ text,
67
58
  color: 'white',
68
59
  };
69
60
  } else {
70
- // Team text → indented with role prefix, concise
71
61
  return {
72
62
  id: ++lineCounter,
73
63
  prefix: event.roleId,
74
64
  prefixColor: roleColor,
75
- text: text.slice(0, 80),
65
+ text,
76
66
  color: 'white',
77
67
  indent: true,
78
68
  };
79
69
  }
80
70
  }
81
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
+
82
85
  case 'dispatch:start': {
83
86
  const target = (event.data.targetRole as string) ?? '';
84
87
  const task = ((event.data.task as string) ?? '');
85
- // Filter out system prompt from task display
86
- const cleanTask = task.replace(/⛔[^⛔]*⛔[^"]*/g, '').trim().slice(0, 50);
88
+ const cleanTask = task.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 80);
87
89
  if (isSupervisor) {
88
90
  return {
89
91
  id: ++lineCounter,
90
- text: `→ ${target} 배정${cleanTask ? ': ' + cleanTask : ''}`,
92
+ text: `\u2192 ${target} \uBC30\uC815${cleanTask ? ': ' + cleanTask : ''}`,
91
93
  color: 'yellow',
92
94
  };
93
95
  }
@@ -95,7 +97,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
95
97
  id: ++lineCounter,
96
98
  prefix: event.roleId,
97
99
  prefixColor: roleColor,
98
- text: `→ ${target} 배정`,
100
+ text: `\u2192 ${target} \uBC30\uC815${cleanTask ? ': ' + cleanTask : ''}`,
99
101
  color: 'yellow',
100
102
  indent: true,
101
103
  };
@@ -105,9 +107,9 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
105
107
  const target = (event.data.targetRole as string) ?? '';
106
108
  return {
107
109
  id: ++lineCounter,
108
- prefix: event.roleId,
110
+ prefix: isSupervisor ? undefined : event.roleId,
109
111
  prefixColor: roleColor,
110
- text: `← ${target} 완료`,
112
+ text: `\u2190 ${target} \uC644\uB8CC`,
111
113
  color: 'yellow',
112
114
  indent: !isSupervisor,
113
115
  };
@@ -119,37 +121,48 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
119
121
  let detail = '';
120
122
  if (input && typeof input === 'object') {
121
123
  const inp = input as Record<string, unknown>;
122
- if (inp.file_path) detail = ` ${String(inp.file_path).split('/').pop()}`;
123
- 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)}`;
124
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
+ }
125
138
 
126
- if (isSupervisor) {
127
- // Supervisor tool use subtle
128
- return {
129
- id: ++lineCounter,
130
- text: ` → ${toolName}${detail}`,
131
- color: 'gray',
132
- };
133
- }
139
+ case 'tool:result': {
140
+ const toolName = (event.data.name as string) ?? 'tool';
134
141
  return {
135
142
  id: ++lineCounter,
136
- prefix: event.roleId,
143
+ prefix: isSupervisor ? undefined : event.roleId,
137
144
  prefixColor: roleColor,
138
- text: `→ ${toolName}${detail}`,
145
+ text: ` \u2190 ${toolName} done`,
139
146
  color: 'gray',
140
- indent: true,
147
+ indent: !isSupervisor,
141
148
  };
142
149
  }
143
150
 
144
151
  case 'msg:start': {
145
- if (isSupervisor) return null; // Hide supervisor start (noise)
146
152
  const task = ((event.data.task as string) ?? '');
147
- const cleanTask = task.replace(/⛔[^⛔]*⛔[^"]*/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
+ }
148
161
  return {
149
162
  id: ++lineCounter,
150
163
  prefix: event.roleId,
151
164
  prefixColor: roleColor,
152
- text: `▶ ${cleanTask || 'started'}`,
165
+ text: `\u25B6 ${cleanTask || 'started'}`,
153
166
  color: 'green',
154
167
  indent: true,
155
168
  };
@@ -157,44 +170,59 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
157
170
 
158
171
  case 'msg:done': {
159
172
  const turns = event.data.turns as number | undefined;
160
- if (isSupervisor) return null; // Hide supervisor done
173
+ if (isSupervisor) {
174
+ return {
175
+ id: ++lineCounter,
176
+ text: `\u2713 Supervisor done${turns ? ` (${turns} turns)` : ''}`,
177
+ color: 'cyan',
178
+ };
179
+ }
161
180
  return {
162
181
  id: ++lineCounter,
163
182
  prefix: event.roleId,
164
183
  prefixColor: roleColor,
165
- text: `✓ done${turns ? ` (${turns} turns)` : ''}`,
184
+ text: `\u2713 done${turns ? ` (${turns} turns)` : ''}`,
166
185
  color: 'green',
167
186
  indent: true,
168
187
  };
169
188
  }
170
189
 
171
190
  case 'msg:error': {
172
- if (isSupervisor) return null; // Supervisor errors handled by system messages
173
- 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
+ }
174
199
  return {
175
200
  id: ++lineCounter,
176
201
  prefix: event.roleId,
177
202
  prefixColor: roleColor,
178
- text: `✗ ${error}`,
203
+ text: `\u2717 ${error}`,
179
204
  color: 'red',
180
205
  indent: true,
181
206
  };
182
207
  }
183
208
 
184
- case 'msg:awaiting_input':
209
+ case 'msg:awaiting_input': {
210
+ const question = (event.data.question as string) ?? '';
185
211
  return {
186
212
  id: ++lineCounter,
187
- text: isSupervisor ? '...' : ` ${event.roleId}: waiting`,
213
+ prefix: isSupervisor ? undefined : event.roleId,
214
+ prefixColor: roleColor,
215
+ text: question ? `? ${question.slice(0, 100)}` : '? Awaiting input...',
188
216
  color: 'yellow',
217
+ indent: !isSupervisor,
189
218
  };
219
+ }
190
220
 
191
- // Hidden
192
- case 'thinking':
221
+ // Hidden (truly internal)
193
222
  case 'heartbeat:tick':
194
223
  case 'heartbeat:skip':
195
224
  case 'prompt:assembled':
196
225
  case 'trace:response':
197
- case 'tool:result':
198
226
  return null;
199
227
 
200
228
  default:
@@ -202,6 +230,21 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
202
230
  }
203
231
  }
204
232
 
233
+ /** Render a single StreamLine */
234
+ function StreamLineRow({ line }: { line: StreamLine }) {
235
+ return (
236
+ <Box>
237
+ {line.indent && <Text> </Text>}
238
+ {line.prefix && (
239
+ <Text color={line.prefixColor} bold>
240
+ {(line.prefix).padEnd(12)}
241
+ </Text>
242
+ )}
243
+ <Text color={line.color} wrap="wrap">{line.text}</Text>
244
+ </Box>
245
+ );
246
+ }
247
+
205
248
  export const CommandMode: React.FC<CommandModeProps> = ({
206
249
  events,
207
250
  allRoleIds,
@@ -209,6 +252,7 @@ export const CommandMode: React.FC<CommandModeProps> = ({
209
252
  onSubmit,
210
253
  }) => {
211
254
  const [input, setInput] = useState('');
255
+ const committedRef = useRef(0);
212
256
 
213
257
  // Convert events to stream lines
214
258
  const eventLines: StreamLine[] = [];
@@ -217,8 +261,18 @@ export const CommandMode: React.FC<CommandModeProps> = ({
217
261
  if (line) eventLines.push(line);
218
262
  }
219
263
 
220
- // Merge system messages and event lines, show last MAX_STREAM_LINES
221
- const allLines = [...systemMessages, ...eventLines].slice(-MAX_STREAM_LINES);
264
+ // Merge system messages and event lines
265
+ const allLines = [...systemMessages, ...eventLines];
266
+
267
+ // Split into committed (scrollback) and live (re-rendered)
268
+ const newCommitted = allLines.slice(committedRef.current);
269
+ if (newCommitted.length > 8) {
270
+ const toCommit = newCommitted.slice(0, -8);
271
+ committedRef.current += toCommit.length;
272
+ }
273
+
274
+ const committedLines = allLines.slice(0, committedRef.current);
275
+ const liveLines = allLines.slice(committedRef.current);
222
276
 
223
277
  const handleSubmit = useCallback((value: string) => {
224
278
  const trimmed = value.trim();
@@ -229,48 +283,35 @@ export const CommandMode: React.FC<CommandModeProps> = ({
229
283
  }, [onSubmit]);
230
284
 
231
285
  return (
232
- <Box flexDirection="column" flexGrow={1}>
233
- {/* Stream area */}
234
- <Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
235
- {allLines.length === 0 && (
236
- <Box marginTop={1}>
237
- <Text color="gray" dimColor>
238
- Type a message to your AI team, or /help for commands.
239
- </Text>
240
- </Box>
241
- )}
242
- {allLines.map((line) => (
243
- <Box key={line.id}>
244
- {line.indent && <Text> </Text>}
245
- {line.prefix && (
246
- <Text color={line.prefixColor} bold>
247
- {(line.prefix).padEnd(12)}
248
- </Text>
249
- )}
250
- <Text color={line.color}>{line.text}</Text>
251
- </Box>
252
- ))}
253
- </Box>
286
+ <Box flexDirection="column">
287
+ {/* Committed lines → pushed to terminal scrollback */}
288
+ <Static items={committedLines}>
289
+ {(line) => <StreamLineRow key={line.id} line={line} />}
290
+ </Static>
254
291
 
255
- {/* Separator */}
256
- <Box width="100%">
257
- <Text color="gray">{'─'.repeat(process.stdout.columns || 70)}</Text>
258
- </Box>
292
+ {/* Live lines → re-rendered */}
293
+ {liveLines.map((line) => (
294
+ <StreamLineRow key={line.id} line={line} />
295
+ ))}
259
296
 
260
- {/* Input */}
261
- <Box paddingX={1} justifyContent="space-between">
297
+ {/* Empty state */}
298
+ {allLines.length === 0 && (
262
299
  <Box>
263
- <Text color="yellow" bold>&gt; </Text>
264
- <TextInput
265
- value={input}
266
- onChange={setInput}
267
- onSubmit={handleSubmit}
268
- placeholder=""
269
- />
270
- </Box>
271
- <Box>
272
- <Text color="gray" dimColor>[Tab] panel</Text>
300
+ <Text color="gray" dimColor>
301
+ Type a message to your AI team, or /help for commands.
302
+ </Text>
273
303
  </Box>
304
+ )}
305
+
306
+ {/* Input */}
307
+ <Box paddingX={0} marginTop={0}>
308
+ <Text color="yellow" bold>&gt; </Text>
309
+ <TextInput
310
+ value={input}
311
+ onChange={setInput}
312
+ onSubmit={handleSubmit}
313
+ placeholder=""
314
+ />
274
315
  </Box>
275
316
  </Box>
276
317
  );
@@ -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
  })}