tycono 0.1.96-beta.2 → 0.1.96-beta.21

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.
@@ -1,5 +1,6 @@
1
1
  /**
2
- * StatusBar — top bar showing company name, wave status, active count, cost
2
+ * StatusBar — bottom bar (Claude Code style)
3
+ * Shows: company name, wave index [focused/total], active roles, ports, cost
3
4
  */
4
5
 
5
6
  import React from 'react';
@@ -7,43 +8,61 @@ import { Box, Text } from 'ink';
7
8
 
8
9
  interface StatusBarProps {
9
10
  companyName: string;
10
- waveId: string | null;
11
+ waveIndex: number; // 1-based focused wave index (0 = none)
12
+ waveCount: number; // total waves
11
13
  waveStatus: 'idle' | 'running' | 'done';
12
14
  activeCount: number;
15
+ portCount: number; // total allocated ports
13
16
  totalCost: number;
14
17
  }
15
18
 
16
19
  export const StatusBar: React.FC<StatusBarProps> = ({
17
20
  companyName,
18
- waveId,
21
+ waveIndex,
22
+ waveCount,
19
23
  waveStatus,
20
24
  activeCount,
25
+ portCount,
21
26
  totalCost,
22
27
  }) => {
23
- const waveLabel = waveId
24
- ? `Wave ${waveId.replace('wave-', '#')} ${waveStatus === 'running' ? '\u25CF' : waveStatus === 'done' ? '\u2713' : ''} ${waveStatus}`
25
- : 'No active wave';
28
+ const statusDot = waveStatus === 'running' ? ' \u25CF'
29
+ : waveStatus === 'done' ? ' \u2713'
30
+ : '';
31
+
32
+ const waveLabel = waveIndex > 0
33
+ ? `Wave ${waveIndex}${statusDot}`
34
+ : '';
35
+
36
+ // Show [1/3] only when 2+ waves
37
+ const countLabel = waveCount >= 2 ? ` [${waveIndex}/${waveCount}]` : '';
26
38
 
27
39
  return (
28
- <Box
29
- width="100%"
30
- paddingX={1}
31
- justifyContent="space-between"
32
- >
33
- <Box>
34
- <Text bold color="cyan">TYCONO</Text>
35
- <Text color="white"> </Text>
36
- <Text color="white">{companyName}</Text>
37
- </Box>
38
- <Box>
39
- <Text color={waveStatus === 'running' ? 'green' : 'gray'}>
40
- {waveLabel}
41
- </Text>
42
- <Text color="white"> </Text>
43
- <Text color="yellow">{activeCount} active</Text>
44
- <Text color="white"> </Text>
45
- <Text color="green">${totalCost.toFixed(2)}</Text>
46
- </Box>
40
+ <Box width="100%" paddingX={1}>
41
+ <Text color="cyan" bold>Tycono</Text>
42
+ <Text color="gray"> | </Text>
43
+ <Text color="white">{companyName}</Text>
44
+ {waveLabel && (
45
+ <>
46
+ <Text color="gray"> | </Text>
47
+ <Text color={waveStatus === 'running' ? 'green' : 'gray'}>
48
+ {waveLabel}{countLabel}
49
+ </Text>
50
+ </>
51
+ )}
52
+ {activeCount > 0 && (
53
+ <>
54
+ <Text color="gray"> | </Text>
55
+ <Text color="yellow">{activeCount} active</Text>
56
+ </>
57
+ )}
58
+ {portCount > 0 && (
59
+ <>
60
+ <Text color="gray"> | </Text>
61
+ <Text color="blue">{portCount} ports</Text>
62
+ </>
63
+ )}
64
+ <Text color="gray"> | </Text>
65
+ <Text color="green">${totalCost.toFixed(2)}</Text>
47
66
  </Box>
48
67
  );
49
68
  };
@@ -1,5 +1,7 @@
1
1
  /**
2
- * StreamPanelright panel showing real-time SSE event stream
2
+ * StreamViewdetailed stream panel for Panel Mode (right side)
3
+ * Shows full event details with timestamps for a selected role.
4
+ * Reuses the rendering logic from StreamPanel v1 but with the v2 layout.
3
5
  */
4
6
 
5
7
  import React from 'react';
@@ -7,12 +9,12 @@ import { Box, Text } from 'ink';
7
9
  import type { SSEEvent } from '../api';
8
10
  import { getRoleColor } from '../theme';
9
11
 
10
- interface StreamPanelProps {
12
+ interface StreamViewProps {
11
13
  events: SSEEvent[];
12
14
  allRoleIds: string[];
13
- focused: boolean;
14
15
  streamStatus: 'idle' | 'streaming' | 'done' | 'error';
15
16
  waveId: string | null;
17
+ roleLabel: string;
16
18
  }
17
19
 
18
20
  function formatTime(ts: string): string {
@@ -24,126 +26,98 @@ function formatTime(ts: string): string {
24
26
  }
25
27
  }
26
28
 
27
- function renderEvent(event: SSEEvent, allRoleIds: string[]): { rolePart: string; roleColor: string; content: string; contentColor: string } {
28
- const roleColor = getRoleColor(event.roleId, allRoleIds);
29
- const rolePart = event.roleId;
30
-
29
+ function renderEvent(event: SSEEvent, allRoleIds: string[]): { content: string; contentColor: string } | null {
31
30
  switch (event.type) {
32
31
  case 'msg:start':
33
32
  return {
34
- rolePart,
35
- roleColor,
36
33
  content: `\u25B6 Started: ${(event.data.task as string)?.slice(0, 60) ?? ''}`,
37
34
  contentColor: 'green',
38
35
  };
39
36
 
40
- case 'msg:done':
37
+ case 'msg:done': {
38
+ const turns = event.data.turns as number | undefined;
41
39
  return {
42
- rolePart,
43
- roleColor,
44
- content: '\u2713 Done',
45
- contentColor: 'gray',
40
+ content: `\u2713 Done${turns ? ` (${turns} turns)` : ''}`,
41
+ contentColor: 'green',
46
42
  };
43
+ }
47
44
 
48
45
  case 'msg:error':
49
46
  return {
50
- rolePart,
51
- roleColor,
52
47
  content: `\u2717 Error: ${(event.data.error as string)?.slice(0, 60) ?? ''}`,
53
48
  contentColor: 'red',
54
49
  };
55
50
 
56
- case 'text':
57
- return {
58
- rolePart,
59
- roleColor,
60
- content: ((event.data.text as string) ?? '').slice(0, 120),
61
- contentColor: 'white',
62
- };
63
-
64
- case 'thinking':
65
- return {
66
- rolePart,
67
- roleColor,
68
- content: `(thinking) ${((event.data.text as string) ?? '').slice(0, 80)}`,
69
- contentColor: 'gray',
70
- };
71
-
72
- case 'tool:start':
51
+ case 'text': {
52
+ const text = ((event.data.text as string) ?? '').slice(0, 120);
53
+ if (!text.trim()) return null;
54
+ return { content: text, contentColor: 'white' };
55
+ }
56
+
57
+ case 'tool:start': {
58
+ const name = (event.data.name as string) ?? 'tool';
59
+ const input = event.data.input;
60
+ let detail = '';
61
+ if (input && typeof input === 'object') {
62
+ const inp = input as Record<string, unknown>;
63
+ 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)}`;
66
+ }
73
67
  return {
74
- rolePart,
75
- roleColor,
76
- content: `\u2192 ${event.data.name as string ?? 'tool'}${event.data.input ? ` ${JSON.stringify(event.data.input).slice(0, 60)}` : ''}`,
68
+ content: `\u2192 ${name}${detail}`,
77
69
  contentColor: 'gray',
78
70
  };
71
+ }
79
72
 
80
73
  case 'tool:result':
81
74
  return {
82
- rolePart,
83
- roleColor,
84
75
  content: `\u2190 ${(event.data.name as string) ?? 'tool'} done`,
85
76
  contentColor: 'gray',
86
77
  };
87
78
 
88
79
  case 'dispatch:start':
89
80
  return {
90
- rolePart,
91
- roleColor,
92
81
  content: `\u21D2 dispatch ${event.data.targetRole as string ?? ''}: ${(event.data.task as string)?.slice(0, 50) ?? ''}`,
93
82
  contentColor: 'yellow',
94
83
  };
95
84
 
96
85
  case 'dispatch:done':
97
86
  return {
98
- rolePart,
99
- roleColor,
100
87
  content: `\u21D0 ${event.data.targetRole as string ?? ''} completed`,
101
88
  contentColor: 'yellow',
102
89
  };
103
90
 
104
91
  case 'msg:awaiting_input':
105
92
  return {
106
- rolePart,
107
- roleColor,
108
93
  content: '? Awaiting input...',
109
94
  contentColor: 'yellow',
110
95
  };
111
96
 
97
+ // Hidden events
98
+ case 'thinking':
112
99
  case 'heartbeat:tick':
113
100
  case 'heartbeat:skip':
114
- return {
115
- rolePart,
116
- roleColor,
117
- content: `\u2665 ${event.type === 'heartbeat:tick' ? 'tick' : 'skip'}`,
118
- contentColor: 'gray',
119
- };
101
+ case 'prompt:assembled':
102
+ case 'trace:response':
103
+ return null;
120
104
 
121
105
  default:
122
- return {
123
- rolePart,
124
- roleColor,
125
- content: event.type,
126
- contentColor: 'gray',
127
- };
106
+ return null;
128
107
  }
129
108
  }
130
109
 
131
- export const StreamPanel: React.FC<StreamPanelProps> = ({
110
+ export const StreamView: React.FC<StreamViewProps> = ({
132
111
  events,
133
112
  allRoleIds,
134
- focused,
135
113
  streamStatus,
136
114
  waveId,
115
+ roleLabel,
137
116
  }) => {
138
- // Show last N events that fit
139
117
  const maxVisible = 20;
140
118
  const visibleEvents = events.slice(-maxVisible);
141
119
 
142
- // Filter out heartbeat noise for display
143
- const displayEvents = visibleEvents.filter(
144
- e => e.type !== 'heartbeat:tick' && e.type !== 'heartbeat:skip'
145
- && e.type !== 'prompt:assembled' && e.type !== 'trace:response'
146
- );
120
+ const turnCount = events.filter(e => e.type === 'text' || e.type === 'tool:start').length;
147
121
 
148
122
  const statusLabel = streamStatus === 'streaming' ? '\u25CF streaming'
149
123
  : streamStatus === 'done' ? '\u2713 done'
@@ -153,27 +127,33 @@ export const StreamPanel: React.FC<StreamPanelProps> = ({
153
127
  return (
154
128
  <Box flexDirection="column" paddingX={1} flexGrow={1}>
155
129
  <Box justifyContent="space-between">
156
- <Text bold color={focused ? 'cyan' : 'gray'}>
157
- {'\u2500\u2500'} Stream {waveId ? `(${waveId})` : ''} {'\u2500\u2500'}
130
+ <Text bold color="cyan">
131
+ Stream ({roleLabel})
132
+ </Text>
133
+ <Text color={streamStatus === 'streaming' ? 'green' : 'gray'}>
134
+ {statusLabel} {turnCount > 0 ? `turn ${turnCount}` : ''}
158
135
  </Text>
159
- <Text color={streamStatus === 'streaming' ? 'green' : 'gray'}>{statusLabel}</Text>
160
136
  </Box>
161
137
 
162
- {displayEvents.length === 0 && (
138
+ {visibleEvents.length === 0 && (
163
139
  <Box marginTop={1}>
164
140
  <Text color="gray" dimColor>
165
- {waveId ? 'Waiting for events...' : 'No active stream. Press [w] to start a wave.'}
141
+ {waveId
142
+ ? `Streaming... waiting for ${roleLabel !== 'All' ? roleLabel + ' ' : ''}events`
143
+ : 'No active stream. Dispatch a wave to start.'}
166
144
  </Text>
167
145
  </Box>
168
146
  )}
169
147
 
170
- {displayEvents.map((event, i) => {
171
- const { rolePart, roleColor, content, contentColor } = renderEvent(event, allRoleIds);
148
+ {visibleEvents.map((event, i) => {
149
+ const rendered = renderEvent(event, allRoleIds);
150
+ if (!rendered) return null;
151
+ const roleColor = getRoleColor(event.roleId, allRoleIds);
172
152
  return (
173
153
  <Box key={`${event.seq}-${i}`}>
174
154
  <Text color="gray" dimColor>{formatTime(event.ts)} </Text>
175
- <Text color={roleColor} bold>{rolePart.padEnd(12)}</Text>
176
- <Text color={contentColor}>{content}</Text>
155
+ <Text color={roleColor} bold>{event.roleId.padEnd(12)}</Text>
156
+ <Text color={rendered.contentColor}>{rendered.content}</Text>
177
157
  </Box>
178
158
  );
179
159
  })}
@@ -8,18 +8,29 @@ import {
8
8
  fetchSessions,
9
9
  fetchExecStatus,
10
10
  fetchActiveWaves,
11
+ fetchActiveSessions,
11
12
  type CompanyInfo,
12
13
  type SessionInfo,
13
14
  type ExecStatus,
15
+ type ActiveSessionInfo,
14
16
  } from '../api';
15
17
 
16
18
  const POLL_INTERVAL = 3000; // 3 seconds
17
19
 
20
+ export interface ActiveWaveInfo {
21
+ waveId: string;
22
+ sessionIds: string[];
23
+ directive?: string;
24
+ startedAt?: number;
25
+ }
26
+
18
27
  export interface ApiState {
19
28
  company: CompanyInfo | null;
20
29
  sessions: SessionInfo[];
21
30
  execStatus: ExecStatus | null;
22
- activeWaveId: string | null;
31
+ activeWaves: ActiveWaveInfo[];
32
+ activeSessions: ActiveSessionInfo[];
33
+ portSummary: { active: number; totalPorts: number };
23
34
  error: string | null;
24
35
  loaded: boolean;
25
36
  refresh(): void;
@@ -29,18 +40,21 @@ export function useApi(): ApiState {
29
40
  const [company, setCompany] = useState<CompanyInfo | null>(null);
30
41
  const [sessions, setSessions] = useState<SessionInfo[]>([]);
31
42
  const [execStatus, setExecStatus] = useState<ExecStatus | null>(null);
32
- const [activeWaveId, setActiveWaveId] = useState<string | null>(null);
43
+ const [activeWaves, setActiveWaves] = useState<ActiveWaveInfo[]>([]);
44
+ const [activeSessions, setActiveSessions] = useState<ActiveSessionInfo[]>([]);
45
+ const [portSummary, setPortSummary] = useState<{ active: number; totalPorts: number }>({ active: 0, totalPorts: 0 });
33
46
  const [error, setError] = useState<string | null>(null);
34
47
  const [loaded, setLoaded] = useState(false);
35
48
  const mountedRef = useRef(true);
36
49
 
37
50
  const refresh = useCallback(async () => {
38
51
  try {
39
- const [comp, sess, exec, waves] = await Promise.all([
52
+ const [comp, sess, exec, waves, activeSess] = await Promise.all([
40
53
  fetchCompany().catch(() => null),
41
54
  fetchSessions().catch(() => []),
42
55
  fetchExecStatus().catch(() => null),
43
56
  fetchActiveWaves().catch(() => ({ waves: [] })),
57
+ fetchActiveSessions().catch(() => ({ sessions: [], summary: { active: 0, totalPorts: 0 } })),
44
58
  ]);
45
59
 
46
60
  if (!mountedRef.current) return;
@@ -49,11 +63,18 @@ export function useApi(): ApiState {
49
63
  setSessions(Array.isArray(sess) ? sess : []);
50
64
  if (exec) setExecStatus(exec);
51
65
 
52
- // Find active wave
66
+ // Store full active waves array
53
67
  if (waves.waves && waves.waves.length > 0) {
54
- setActiveWaveId(waves.waves[0].waveId);
68
+ setActiveWaves(waves.waves.map((w: { waveId: string; sessionIds: string[] }) => ({
69
+ waveId: w.waveId,
70
+ sessionIds: w.sessionIds ?? [],
71
+ })));
55
72
  }
56
73
 
74
+ // Active sessions (port/resource visibility)
75
+ setActiveSessions(activeSess.sessions ?? []);
76
+ setPortSummary(activeSess.summary ?? { active: 0, totalPorts: 0 });
77
+
57
78
  setError(null);
58
79
  setLoaded(true);
59
80
  } catch (err) {
@@ -73,5 +94,5 @@ export function useApi(): ApiState {
73
94
  };
74
95
  }, [refresh]);
75
96
 
76
- return { company, sessions, execStatus, activeWaveId, error, loaded, refresh };
97
+ return { company, sessions, execStatus, activeWaves, activeSessions, portSummary, error, loaded, refresh };
77
98
  }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * useCommand — input handler for TUI v2 (Multi-Wave)
3
+ *
4
+ * Default: natural language → sendDirective to focused wave
5
+ * Commands (/ prefix):
6
+ * /waves — list all waves
7
+ * /focus <n> — switch to nth wave
8
+ * /new [text] — create new wave (optionally with directive)
9
+ * /agents — show wave→agent tree with resources
10
+ * /ports — show port allocations
11
+ * /status — show current wave + session status
12
+ * /assign <role> <task> — assign task to specific role
13
+ * /roles — show org tree (Panel Mode)
14
+ * /help — show help
15
+ * /quit — exit
16
+ */
17
+
18
+ import { useCallback } from 'react';
19
+ import { dispatchWave, sendDirective, fetchJson } from '../api';
20
+
21
+ export interface WaveInfo {
22
+ waveId: string;
23
+ directive: string;
24
+ startedAt: number;
25
+ }
26
+
27
+ export interface CommandResult {
28
+ type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel' | 'waves_list' | 'focus_changed' | 'agents' | 'ports';
29
+ message: string;
30
+ waveId?: string;
31
+ }
32
+
33
+ async function postAssign(roleId: string, task: string): Promise<{ waveId?: string }> {
34
+ return fetchJson<{ waveId?: string }>('/api/jobs', {
35
+ method: 'POST',
36
+ body: {
37
+ type: 'assign',
38
+ roleId,
39
+ task,
40
+ },
41
+ });
42
+ }
43
+
44
+ export interface UseCommandOptions {
45
+ focusedWaveId: string | null;
46
+ waves: WaveInfo[];
47
+ onWaveCreated: (waveId: string, directive: string) => void;
48
+ onFocusWave: (waveId: string) => void;
49
+ onQuit: () => void;
50
+ onShowPanel: () => void;
51
+ }
52
+
53
+ export function useCommand(options: UseCommandOptions) {
54
+ const { focusedWaveId, waves, onWaveCreated, onFocusWave, onQuit, onShowPanel } = options;
55
+
56
+ const execute = useCallback(async (input: string): Promise<CommandResult> => {
57
+ const trimmed = input.trim();
58
+ if (!trimmed) return { type: 'info', message: '' };
59
+
60
+ // Slash commands
61
+ if (trimmed.startsWith('/')) {
62
+ const parts = trimmed.slice(1).split(/\s+/);
63
+ const cmd = parts[0].toLowerCase();
64
+ const args = parts.slice(1).join(' ');
65
+
66
+ switch (cmd) {
67
+ case 'waves': {
68
+ return { type: 'waves_list', message: '__waves__' };
69
+ }
70
+
71
+ case 'focus': {
72
+ const idx = parseInt(args, 10);
73
+ if (isNaN(idx) || idx < 1 || idx > waves.length) {
74
+ return { type: 'error', message: `Usage: /focus <1-${waves.length}>` };
75
+ }
76
+ const target = waves[idx - 1];
77
+ onFocusWave(target.waveId);
78
+ return { type: 'focus_changed', message: `Focused on Wave ${idx}`, waveId: target.waveId };
79
+ }
80
+
81
+ case 'new': {
82
+ const directive = args || undefined;
83
+ try {
84
+ const result = await dispatchWave(directive);
85
+ onWaveCreated(result.waveId, directive ?? '');
86
+ return {
87
+ type: 'wave_started',
88
+ message: `Wave created`,
89
+ waveId: result.waveId,
90
+ };
91
+ } catch (err) {
92
+ return { type: 'error', message: `New wave failed: ${err instanceof Error ? err.message : 'unknown'}` };
93
+ }
94
+ }
95
+
96
+ case 'agents':
97
+ return { type: 'agents', message: '__agents__' };
98
+
99
+ case 'ports':
100
+ return { type: 'ports', message: '__ports__' };
101
+
102
+ case 'status':
103
+ return { type: 'info', message: '__status__' };
104
+
105
+ case 'assign': {
106
+ const spaceIdx = args.indexOf(' ');
107
+ if (spaceIdx === -1 || !args) {
108
+ return { type: 'error', message: 'Usage: /assign <role> <task>' };
109
+ }
110
+ const roleId = args.slice(0, spaceIdx);
111
+ const task = args.slice(spaceIdx + 1);
112
+ try {
113
+ const result = await postAssign(roleId, task);
114
+ return { type: 'success', message: `Task assigned to ${roleId}`, waveId: result.waveId };
115
+ } catch (err) {
116
+ return { type: 'error', message: `Assign failed: ${err instanceof Error ? err.message : 'unknown'}` };
117
+ }
118
+ }
119
+
120
+ case 'roles':
121
+ onShowPanel();
122
+ return { type: 'panel', message: '' };
123
+
124
+ case 'help':
125
+ return { type: 'help', message: '__help__' };
126
+
127
+ case 'quit':
128
+ case 'exit':
129
+ onQuit();
130
+ return { type: 'quit', message: 'Goodbye!' };
131
+
132
+ default:
133
+ return { type: 'error', message: `Unknown command: /${cmd}. Type /help for commands.` };
134
+ }
135
+ }
136
+
137
+ // Default: natural language → directive to focused wave
138
+ if (focusedWaveId) {
139
+ try {
140
+ await sendDirective(focusedWaveId, trimmed);
141
+ return { type: 'directive_sent', message: `Directive sent` };
142
+ } catch (err) {
143
+ return { type: 'error', message: `Failed: ${err instanceof Error ? err.message : 'unknown'}` };
144
+ }
145
+ } else {
146
+ // No focused wave — create one with the directive
147
+ try {
148
+ const result = await dispatchWave(trimmed);
149
+ onWaveCreated(result.waveId, trimmed);
150
+ return {
151
+ type: 'wave_started',
152
+ message: `Wave created`,
153
+ waveId: result.waveId,
154
+ };
155
+ } catch (err) {
156
+ return { type: 'error', message: `Wave failed: ${err instanceof Error ? err.message : 'unknown'}` };
157
+ }
158
+ }
159
+ }, [focusedWaveId, waves, onWaveCreated, onFocusWave, onQuit, onShowPanel]);
160
+
161
+ return { execute };
162
+ }