tycono 0.1.96-beta.21 → 0.1.96-beta.22
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 +77 -69
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,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CommandMode —
|
|
2
|
+
* CommandMode — scrollable terminal mode (like Claude Code)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - System prompts, internal noise: filtered out
|
|
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.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
import React, { useState, useCallback } from 'react';
|
|
11
|
-
import { Box, Text } from 'ink';
|
|
9
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
10
|
+
import { Box, Text, Static } from 'ink';
|
|
12
11
|
import TextInput from 'ink-text-input';
|
|
13
12
|
import type { SSEEvent } from '../api';
|
|
14
13
|
import { getRoleColor } from '../theme';
|
|
15
14
|
|
|
16
|
-
const MAX_STREAM_LINES = 30;
|
|
17
15
|
const SUPERVISOR_ROLE = 'ceo';
|
|
18
16
|
|
|
19
17
|
export interface StreamLine {
|
|
@@ -38,14 +36,13 @@ let lineCounter = 0;
|
|
|
38
36
|
function isSystemNoise(text: string): boolean {
|
|
39
37
|
const t = text.trim();
|
|
40
38
|
if (!t) return true;
|
|
41
|
-
// System prompt fragments
|
|
42
39
|
if (t.startsWith('## Your Role')) return true;
|
|
43
40
|
if (t.startsWith('You are')) return true;
|
|
44
41
|
if (t.startsWith('[CEO Supervisor]')) return true;
|
|
45
42
|
if (t.startsWith('[Question from')) return true;
|
|
46
|
-
if (t.includes('
|
|
47
|
-
if (t.includes('
|
|
48
|
-
if (t.startsWith('
|
|
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;
|
|
49
46
|
return false;
|
|
50
47
|
}
|
|
51
48
|
|
|
@@ -60,14 +57,12 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
60
57
|
if (isSystemNoise(text)) return null;
|
|
61
58
|
|
|
62
59
|
if (isSupervisor) {
|
|
63
|
-
// Supervisor text → direct response (no prefix, generous length)
|
|
64
60
|
return {
|
|
65
61
|
id: ++lineCounter,
|
|
66
62
|
text: text.slice(0, 200),
|
|
67
63
|
color: 'white',
|
|
68
64
|
};
|
|
69
65
|
} else {
|
|
70
|
-
// Team text → indented with role prefix, concise
|
|
71
66
|
return {
|
|
72
67
|
id: ++lineCounter,
|
|
73
68
|
prefix: event.roleId,
|
|
@@ -82,12 +77,11 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
82
77
|
case 'dispatch:start': {
|
|
83
78
|
const target = (event.data.targetRole as string) ?? '';
|
|
84
79
|
const task = ((event.data.task as string) ?? '');
|
|
85
|
-
|
|
86
|
-
const cleanTask = task.replace(/⛔[^⛔]*⛔[^"]*/g, '').trim().slice(0, 50);
|
|
80
|
+
const cleanTask = task.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 50);
|
|
87
81
|
if (isSupervisor) {
|
|
88
82
|
return {
|
|
89
83
|
id: ++lineCounter,
|
|
90
|
-
text:
|
|
84
|
+
text: `\u2192 ${target} \uBC30\uC815${cleanTask ? ': ' + cleanTask : ''}`,
|
|
91
85
|
color: 'yellow',
|
|
92
86
|
};
|
|
93
87
|
}
|
|
@@ -95,7 +89,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
95
89
|
id: ++lineCounter,
|
|
96
90
|
prefix: event.roleId,
|
|
97
91
|
prefixColor: roleColor,
|
|
98
|
-
text:
|
|
92
|
+
text: `\u2192 ${target} \uBC30\uC815`,
|
|
99
93
|
color: 'yellow',
|
|
100
94
|
indent: true,
|
|
101
95
|
};
|
|
@@ -107,7 +101,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
107
101
|
id: ++lineCounter,
|
|
108
102
|
prefix: event.roleId,
|
|
109
103
|
prefixColor: roleColor,
|
|
110
|
-
text:
|
|
104
|
+
text: `\u2190 ${target} \uC644\uB8CC`,
|
|
111
105
|
color: 'yellow',
|
|
112
106
|
indent: !isSupervisor,
|
|
113
107
|
};
|
|
@@ -124,10 +118,9 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
124
118
|
}
|
|
125
119
|
|
|
126
120
|
if (isSupervisor) {
|
|
127
|
-
// Supervisor tool use → subtle
|
|
128
121
|
return {
|
|
129
122
|
id: ++lineCounter,
|
|
130
|
-
text: `
|
|
123
|
+
text: ` \u2192 ${toolName}${detail}`,
|
|
131
124
|
color: 'gray',
|
|
132
125
|
};
|
|
133
126
|
}
|
|
@@ -135,21 +128,21 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
135
128
|
id: ++lineCounter,
|
|
136
129
|
prefix: event.roleId,
|
|
137
130
|
prefixColor: roleColor,
|
|
138
|
-
text:
|
|
131
|
+
text: `\u2192 ${toolName}${detail}`,
|
|
139
132
|
color: 'gray',
|
|
140
133
|
indent: true,
|
|
141
134
|
};
|
|
142
135
|
}
|
|
143
136
|
|
|
144
137
|
case 'msg:start': {
|
|
145
|
-
if (isSupervisor) return null;
|
|
138
|
+
if (isSupervisor) return null;
|
|
146
139
|
const task = ((event.data.task as string) ?? '');
|
|
147
|
-
const cleanTask = task.replace(
|
|
140
|
+
const cleanTask = task.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 40);
|
|
148
141
|
return {
|
|
149
142
|
id: ++lineCounter,
|
|
150
143
|
prefix: event.roleId,
|
|
151
144
|
prefixColor: roleColor,
|
|
152
|
-
text:
|
|
145
|
+
text: `\u25B6 ${cleanTask || 'started'}`,
|
|
153
146
|
color: 'green',
|
|
154
147
|
indent: true,
|
|
155
148
|
};
|
|
@@ -157,25 +150,25 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
157
150
|
|
|
158
151
|
case 'msg:done': {
|
|
159
152
|
const turns = event.data.turns as number | undefined;
|
|
160
|
-
if (isSupervisor) return null;
|
|
153
|
+
if (isSupervisor) return null;
|
|
161
154
|
return {
|
|
162
155
|
id: ++lineCounter,
|
|
163
156
|
prefix: event.roleId,
|
|
164
157
|
prefixColor: roleColor,
|
|
165
|
-
text:
|
|
158
|
+
text: `\u2713 done${turns ? ` (${turns} turns)` : ''}`,
|
|
166
159
|
color: 'green',
|
|
167
160
|
indent: true,
|
|
168
161
|
};
|
|
169
162
|
}
|
|
170
163
|
|
|
171
164
|
case 'msg:error': {
|
|
172
|
-
if (isSupervisor) return null;
|
|
165
|
+
if (isSupervisor) return null;
|
|
173
166
|
const error = ((event.data.error as string) ?? '').slice(0, 60);
|
|
174
167
|
return {
|
|
175
168
|
id: ++lineCounter,
|
|
176
169
|
prefix: event.roleId,
|
|
177
170
|
prefixColor: roleColor,
|
|
178
|
-
text:
|
|
171
|
+
text: `\u2717 ${error}`,
|
|
179
172
|
color: 'red',
|
|
180
173
|
indent: true,
|
|
181
174
|
};
|
|
@@ -202,6 +195,21 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
202
195
|
}
|
|
203
196
|
}
|
|
204
197
|
|
|
198
|
+
/** Render a single StreamLine */
|
|
199
|
+
function StreamLineRow({ line }: { line: StreamLine }) {
|
|
200
|
+
return (
|
|
201
|
+
<Box>
|
|
202
|
+
{line.indent && <Text> </Text>}
|
|
203
|
+
{line.prefix && (
|
|
204
|
+
<Text color={line.prefixColor} bold>
|
|
205
|
+
{(line.prefix).padEnd(12)}
|
|
206
|
+
</Text>
|
|
207
|
+
)}
|
|
208
|
+
<Text color={line.color}>{line.text}</Text>
|
|
209
|
+
</Box>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
205
213
|
export const CommandMode: React.FC<CommandModeProps> = ({
|
|
206
214
|
events,
|
|
207
215
|
allRoleIds,
|
|
@@ -209,6 +217,7 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
209
217
|
onSubmit,
|
|
210
218
|
}) => {
|
|
211
219
|
const [input, setInput] = useState('');
|
|
220
|
+
const committedRef = useRef(0);
|
|
212
221
|
|
|
213
222
|
// Convert events to stream lines
|
|
214
223
|
const eventLines: StreamLine[] = [];
|
|
@@ -217,8 +226,20 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
217
226
|
if (line) eventLines.push(line);
|
|
218
227
|
}
|
|
219
228
|
|
|
220
|
-
// Merge system messages and event lines
|
|
221
|
-
const allLines = [...systemMessages, ...eventLines]
|
|
229
|
+
// Merge system messages and event lines
|
|
230
|
+
const allLines = [...systemMessages, ...eventLines];
|
|
231
|
+
|
|
232
|
+
// Split into committed (scrollback) and live (re-rendered)
|
|
233
|
+
// Lines up to committedRef are frozen in scrollback
|
|
234
|
+
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);
|
|
238
|
+
committedRef.current += toCommit.length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const committedLines = allLines.slice(0, committedRef.current);
|
|
242
|
+
const liveLines = allLines.slice(committedRef.current);
|
|
222
243
|
|
|
223
244
|
const handleSubmit = useCallback((value: string) => {
|
|
224
245
|
const trimmed = value.trim();
|
|
@@ -229,48 +250,35 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
229
250
|
}, [onSubmit]);
|
|
230
251
|
|
|
231
252
|
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>
|
|
253
|
+
<Box flexDirection="column">
|
|
254
|
+
{/* Committed lines → pushed to terminal scrollback (scrollable with mouse wheel) */}
|
|
255
|
+
<Static items={committedLines}>
|
|
256
|
+
{(line) => <StreamLineRow key={line.id} line={line} />}
|
|
257
|
+
</Static>
|
|
254
258
|
|
|
255
|
-
{/*
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
+
{/* Live lines → re-rendered on each update */}
|
|
260
|
+
{liveLines.map((line) => (
|
|
261
|
+
<StreamLineRow key={line.id} line={line} />
|
|
262
|
+
))}
|
|
259
263
|
|
|
260
|
-
{/*
|
|
261
|
-
|
|
264
|
+
{/* Empty state */}
|
|
265
|
+
{allLines.length === 0 && (
|
|
262
266
|
<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>
|
|
267
|
+
<Text color="gray" dimColor>
|
|
268
|
+
Type a message to your AI team, or /help for commands.
|
|
269
|
+
</Text>
|
|
273
270
|
</Box>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* Input */}
|
|
274
|
+
<Box paddingX={0} marginTop={0}>
|
|
275
|
+
<Text color="yellow" bold>> </Text>
|
|
276
|
+
<TextInput
|
|
277
|
+
value={input}
|
|
278
|
+
onChange={setInput}
|
|
279
|
+
onSubmit={handleSubmit}
|
|
280
|
+
placeholder=""
|
|
281
|
+
/>
|
|
274
282
|
</Box>
|
|
275
283
|
</Box>
|
|
276
284
|
);
|