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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.96-beta.21",
3
+ "version": "0.1.96-beta.22",
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,17 @@
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
+ * 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(' AKB Rule')) return true;
47
- if (t.includes(' Read the')) return true;
48
- if (t.startsWith('')) 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;
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
- // Filter out system prompt from task display
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: `→ ${target} 배정${cleanTask ? ': ' + cleanTask : ''}`,
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: `→ ${target} 배정`,
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: `← ${target} 완료`,
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: ` ${toolName}${detail}`,
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: `→ ${toolName}${detail}`,
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; // Hide supervisor start (noise)
138
+ if (isSupervisor) return null;
146
139
  const task = ((event.data.task as string) ?? '');
147
- const cleanTask = task.replace(/⛔[^⛔]*⛔[^"]*/g, '').trim().slice(0, 40);
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: `▶ ${cleanTask || 'started'}`,
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; // Hide supervisor done
153
+ if (isSupervisor) return null;
161
154
  return {
162
155
  id: ++lineCounter,
163
156
  prefix: event.roleId,
164
157
  prefixColor: roleColor,
165
- text: `✓ done${turns ? ` (${turns} turns)` : ''}`,
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; // Supervisor errors handled by system messages
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: `✗ ${error}`,
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, show last MAX_STREAM_LINES
221
- const allLines = [...systemMessages, ...eventLines].slice(-MAX_STREAM_LINES);
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" 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>
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
- {/* Separator */}
256
- <Box width="100%">
257
- <Text color="gray">{'─'.repeat(process.stdout.columns || 70)}</Text>
258
- </Box>
259
+ {/* Live lines → re-rendered on each update */}
260
+ {liveLines.map((line) => (
261
+ <StreamLineRow key={line.id} line={line} />
262
+ ))}
259
263
 
260
- {/* Input */}
261
- <Box paddingX={1} justifyContent="space-between">
264
+ {/* Empty state */}
265
+ {allLines.length === 0 && (
262
266
  <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>
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>&gt; </Text>
276
+ <TextInput
277
+ value={input}
278
+ onChange={setInput}
279
+ onSubmit={handleSubmit}
280
+ placeholder=""
281
+ />
274
282
  </Box>
275
283
  </Box>
276
284
  );