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.
- package/README.md +106 -35
- package/bin/tycono.ts +23 -1
- package/package.json +1 -1
- package/src/api/src/routes/active-sessions.ts +3 -0
- package/src/api/src/routes/execute.ts +6 -12
- package/src/api/src/services/supervisor-heartbeat.ts +19 -0
- package/src/tui/api.ts +40 -7
- package/src/tui/app.tsx +369 -173
- package/src/tui/components/CommandMode.tsx +277 -0
- package/src/tui/components/OrgTree.tsx +4 -17
- package/src/tui/components/PanelMode.tsx +265 -0
- package/src/tui/components/SetupWizard.tsx +10 -3
- package/src/tui/components/StatusBar.tsx +44 -25
- package/src/tui/components/{StreamPanel.tsx → StreamView.tsx} +53 -73
- package/src/tui/hooks/useApi.ts +27 -6
- package/src/tui/hooks/useCommand.ts +162 -0
- package/src/tui/hooks/useSSE.ts +68 -24
- package/src/tui/store.ts +12 -0
- package/src/tui/components/CommandInput.tsx +0 -32
- package/src/tui/components/HelpOverlay.tsx +0 -51
- package/src/tui/components/SessionList.tsx +0 -74
- package/src/tui/components/WaveDialog.tsx +0 -56
- package/src/tui/hooks/useKeyboard.ts +0 -62
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StatusBar —
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
waveIndex,
|
|
22
|
+
waveCount,
|
|
19
23
|
waveStatus,
|
|
20
24
|
activeCount,
|
|
25
|
+
portCount,
|
|
21
26
|
totalCost,
|
|
22
27
|
}) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
: '
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
*
|
|
2
|
+
* StreamView — detailed 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
|
|
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[]): {
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
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=
|
|
157
|
-
|
|
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
|
-
{
|
|
138
|
+
{visibleEvents.length === 0 && (
|
|
163
139
|
<Box marginTop={1}>
|
|
164
140
|
<Text color="gray" dimColor>
|
|
165
|
-
{waveId
|
|
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
|
-
{
|
|
171
|
-
const
|
|
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>{
|
|
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
|
})}
|
package/src/tui/hooks/useApi.ts
CHANGED
|
@@ -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
|
-
|
|
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 [
|
|
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
|
-
//
|
|
66
|
+
// Store full active waves array
|
|
53
67
|
if (waves.waves && waves.waves.length > 0) {
|
|
54
|
-
|
|
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,
|
|
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
|
+
}
|