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 +1 -1
- package/src/tui/app.tsx +51 -38
- package/src/tui/components/CommandMode.tsx +134 -93
- package/src/tui/components/StreamView.tsx +24 -15
package/package.json
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -407,47 +407,60 @@ export const App: React.FC = () => {
|
|
|
407
407
|
);
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
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
|
-
|
|
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 —
|
|
2
|
+
* CommandMode — scrollable terminal mode (like Claude Code)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
-
//
|
|
42
|
-
if (t.startsWith('
|
|
43
|
-
if (t.startsWith('
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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)
|
|
123
|
-
else if (inp.command) detail = ` ${String(inp.command).slice(0,
|
|
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
|
-
|
|
127
|
-
|
|
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:
|
|
145
|
+
text: ` \u2190 ${toolName} done`,
|
|
139
146
|
color: 'gray',
|
|
140
|
-
indent:
|
|
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(
|
|
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:
|
|
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)
|
|
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:
|
|
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
|
-
|
|
173
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
221
|
-
const allLines = [...systemMessages, ...eventLines]
|
|
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"
|
|
233
|
-
{/*
|
|
234
|
-
<
|
|
235
|
-
{
|
|
236
|
-
|
|
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
|
-
{/*
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
|
|
292
|
+
{/* Live lines → re-rendered */}
|
|
293
|
+
{liveLines.map((line) => (
|
|
294
|
+
<StreamLineRow key={line.id} line={line} />
|
|
295
|
+
))}
|
|
259
296
|
|
|
260
|
-
{/*
|
|
261
|
-
|
|
297
|
+
{/* Empty state */}
|
|
298
|
+
{allLines.length === 0 && (
|
|
262
299
|
<Box>
|
|
263
|
-
<Text color="
|
|
264
|
-
|
|
265
|
-
|
|
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>> </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
|
-
*
|
|
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
|
|
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,
|
|
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)
|
|
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) ?? '')
|
|
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,
|
|
65
|
-
else detail = ` ${
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
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
|
})}
|