tycono 0.1.96-beta.13 → 0.1.96-beta.15

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.13",
3
+ "version": "0.1.96-beta.15",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/app.tsx CHANGED
@@ -134,10 +134,10 @@ export const App: React.FC = () => {
134
134
 
135
135
  switch (result.type) {
136
136
  case 'wave_started':
137
- addSystemMessage(`\u26A1 ${result.message}`, 'yellow');
137
+ // Don't show wave ID noise — supervisor handles it
138
138
  break;
139
139
  case 'directive_sent':
140
- addSystemMessage(`\u26A1 ${result.message}`, 'yellow');
140
+ // Silently sent — supervisor will respond
141
141
  break;
142
142
  case 'stopped':
143
143
  addSystemMessage(`\u26A1 ${result.message}`, 'red');
@@ -146,16 +146,14 @@ export const App: React.FC = () => {
146
146
  addSystemMessage(result.message, 'red');
147
147
  break;
148
148
  case 'help':
149
- addSystemMessage('Commands:', 'cyan');
150
- addSystemMessage(' wave <directive> [--continuous] Start a wave', 'white');
151
- addSystemMessage(' directive <text> Send directive to active wave', 'white');
152
- addSystemMessage(' stop Stop all active executions', 'white');
153
- addSystemMessage(' status Show current status', 'white');
154
- addSystemMessage(' assign <role> <task> Assign task to role', 'white');
155
- addSystemMessage(' summary Show last wave summary', 'white');
156
- addSystemMessage(' roles Show org tree (Panel Mode)', 'white');
157
- addSystemMessage(' help Show this help', 'white');
158
- addSystemMessage(' quit Exit TUI', 'white');
149
+ addSystemMessage('Type naturally to talk to your AI team.', 'cyan');
150
+ addSystemMessage('Commands (/ prefix):', 'cyan');
151
+ addSystemMessage(' /stop Stop all executions', 'white');
152
+ addSystemMessage(' /status Show current status', 'white');
153
+ addSystemMessage(' /assign <role> <task> Assign task to role', 'white');
154
+ addSystemMessage(' /roles Org tree (Panel Mode)', 'white');
155
+ addSystemMessage(' /help Show this help', 'white');
156
+ addSystemMessage(' /quit Exit TUI', 'white');
159
157
  addSystemMessage('Keys: [Tab] panel [Esc] back [Ctrl+C] stop/quit', 'gray');
160
158
  break;
161
159
  case 'info':
@@ -231,20 +229,6 @@ export const App: React.FC = () => {
231
229
 
232
230
  return (
233
231
  <Box flexDirection="column" height={termHeight}>
234
- {/* Status Bar — always shown */}
235
- <StatusBar
236
- companyName={api.company?.name ?? 'Loading...'}
237
- waveId={effectiveWaveId}
238
- waveStatus={derivedWaveStatus}
239
- activeCount={activeCount}
240
- totalCost={0}
241
- />
242
-
243
- {/* Separator */}
244
- <Box width="100%">
245
- <Text color="gray">{'─'.repeat(process.stdout.columns || 70)}</Text>
246
- </Box>
247
-
248
232
  {/* Mode content — fill remaining height */}
249
233
  <Box flexGrow={1} flexDirection="column">
250
234
  {mode === 'command' ? (
@@ -278,6 +262,15 @@ export const App: React.FC = () => {
278
262
  />
279
263
  )}
280
264
  </Box>
265
+
266
+ {/* Status Bar — bottom (Claude Code style) */}
267
+ <StatusBar
268
+ companyName={api.company?.name ?? 'Loading...'}
269
+ waveId={effectiveWaveId}
270
+ waveStatus={derivedWaveStatus}
271
+ activeCount={activeCount}
272
+ totalCost={0}
273
+ />
281
274
  </Box>
282
275
  );
283
276
  };
@@ -1,13 +1,10 @@
1
1
  /**
2
- * CommandMode — default mode: stream summary flowing above, command input below
2
+ * CommandMode — chat-first mode
3
3
  *
4
- * SSE events are compressed into one-line summaries:
5
- * dispatch:start "CEO -> CTO Su (supervising...)"
6
- * text → "FE: Canvas game loop implementation..."
7
- * tool:start → "FE: -> edit index.html +87 lines"
8
- * msg:done → "QA: done (12 turns)"
9
- * msg:error → "FE: TypeError at line 42"
10
- * thinking, heartbeat, trace → hidden
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
11
8
  */
12
9
 
13
10
  import React, { useState, useCallback } from 'react';
@@ -16,7 +13,8 @@ import TextInput from 'ink-text-input';
16
13
  import type { SSEEvent } from '../api';
17
14
  import { getRoleColor } from '../theme';
18
15
 
19
- const MAX_STREAM_LINES = 20;
16
+ const MAX_STREAM_LINES = 30;
17
+ const SUPERVISOR_ROLE = 'ceo';
20
18
 
21
19
  export interface StreamLine {
22
20
  id: number;
@@ -24,6 +22,7 @@ export interface StreamLine {
24
22
  color: string;
25
23
  prefix?: string;
26
24
  prefixColor?: string;
25
+ indent?: boolean;
27
26
  }
28
27
 
29
28
  interface CommandModeProps {
@@ -35,21 +34,70 @@ interface CommandModeProps {
35
34
 
36
35
  let lineCounter = 0;
37
36
 
38
- /** Convert SSE event to a one-line summary. Returns null if event should be hidden. */
37
+ /** Filter out system prompt noise from text */
38
+ function isSystemNoise(text: string): boolean {
39
+ const t = text.trim();
40
+ if (!t) return true;
41
+ // System prompt fragments
42
+ if (t.startsWith('## Your Role')) return true;
43
+ if (t.startsWith('You are')) return true;
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;
49
+ return false;
50
+ }
51
+
52
+ /** Convert SSE event to stream lines */
39
53
  export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLine | null {
54
+ const isSupervisor = event.roleId === SUPERVISOR_ROLE;
40
55
  const roleColor = getRoleColor(event.roleId, allRoleIds);
41
- const roleName = event.roleId;
42
56
 
43
57
  switch (event.type) {
58
+ case 'text': {
59
+ const text = ((event.data.text as string) ?? '');
60
+ if (isSystemNoise(text)) return null;
61
+
62
+ if (isSupervisor) {
63
+ // Supervisor text → direct response (no prefix, generous length)
64
+ return {
65
+ id: ++lineCounter,
66
+ text: text.slice(0, 200),
67
+ color: 'white',
68
+ };
69
+ } else {
70
+ // Team text → indented with role prefix, concise
71
+ return {
72
+ id: ++lineCounter,
73
+ prefix: event.roleId,
74
+ prefixColor: roleColor,
75
+ text: text.slice(0, 80),
76
+ color: 'white',
77
+ indent: true,
78
+ };
79
+ }
80
+ }
81
+
44
82
  case 'dispatch:start': {
45
83
  const target = (event.data.targetRole as string) ?? '';
46
- const task = ((event.data.task as string) ?? '').slice(0, 50);
84
+ 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);
87
+ if (isSupervisor) {
88
+ return {
89
+ id: ++lineCounter,
90
+ text: `→ ${target} 배정${cleanTask ? ': ' + cleanTask : ''}`,
91
+ color: 'yellow',
92
+ };
93
+ }
47
94
  return {
48
95
  id: ++lineCounter,
49
- prefix: roleName,
96
+ prefix: event.roleId,
50
97
  prefixColor: roleColor,
51
- text: `\u2192 ${target} (${task || 'dispatching...'})`,
98
+ text: `→ ${target} 배정`,
52
99
  color: 'yellow',
100
+ indent: true,
53
101
  };
54
102
  }
55
103
 
@@ -57,22 +105,11 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
57
105
  const target = (event.data.targetRole as string) ?? '';
58
106
  return {
59
107
  id: ++lineCounter,
60
- prefix: roleName,
108
+ prefix: event.roleId,
61
109
  prefixColor: roleColor,
62
- text: `\u2190 ${target} completed`,
110
+ text: `← ${target} 완료`,
63
111
  color: 'yellow',
64
- };
65
- }
66
-
67
- case 'text': {
68
- const text = ((event.data.text as string) ?? '').slice(0, 60);
69
- if (!text.trim()) return null;
70
- return {
71
- id: ++lineCounter,
72
- prefix: roleName,
73
- prefixColor: roleColor,
74
- text,
75
- color: 'white',
112
+ indent: !isSupervisor,
76
113
  };
77
114
  }
78
115
 
@@ -85,35 +122,49 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
85
122
  if (inp.file_path) detail = ` ${String(inp.file_path).split('/').pop()}`;
86
123
  else if (inp.command) detail = ` ${String(inp.command).slice(0, 40)}`;
87
124
  }
125
+
126
+ if (isSupervisor) {
127
+ // Supervisor tool use → subtle
128
+ return {
129
+ id: ++lineCounter,
130
+ text: ` → ${toolName}${detail}`,
131
+ color: 'gray',
132
+ };
133
+ }
88
134
  return {
89
135
  id: ++lineCounter,
90
- prefix: roleName,
136
+ prefix: event.roleId,
91
137
  prefixColor: roleColor,
92
- text: `\u2192 ${toolName}${detail}`,
138
+ text: `→ ${toolName}${detail}`,
93
139
  color: 'gray',
140
+ indent: true,
94
141
  };
95
142
  }
96
143
 
97
144
  case 'msg:start': {
98
- const task = ((event.data.task as string) ?? '').slice(0, 50);
145
+ if (isSupervisor) return null; // Hide supervisor start (noise)
146
+ const task = ((event.data.task as string) ?? '');
147
+ const cleanTask = task.replace(/⛔[^⛔]*⛔[^"]*/g, '').trim().slice(0, 40);
99
148
  return {
100
149
  id: ++lineCounter,
101
- prefix: roleName,
150
+ prefix: event.roleId,
102
151
  prefixColor: roleColor,
103
- text: `\u25B6 ${task || 'started'}`,
152
+ text: `▶ ${cleanTask || 'started'}`,
104
153
  color: 'green',
154
+ indent: true,
105
155
  };
106
156
  }
107
157
 
108
158
  case 'msg:done': {
109
159
  const turns = event.data.turns as number | undefined;
110
- const turnLabel = turns ? ` (${turns} turns)` : '';
160
+ if (isSupervisor) return null; // Hide supervisor done
111
161
  return {
112
162
  id: ++lineCounter,
113
- prefix: roleName,
163
+ prefix: event.roleId,
114
164
  prefixColor: roleColor,
115
- text: `\u2713 done${turnLabel}`,
165
+ text: `✓ done${turns ? ` (${turns} turns)` : ''}`,
116
166
  color: 'green',
167
+ indent: true,
117
168
  };
118
169
  }
119
170
 
@@ -121,24 +172,22 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
121
172
  const error = ((event.data.error as string) ?? '').slice(0, 60);
122
173
  return {
123
174
  id: ++lineCounter,
124
- prefix: roleName,
175
+ prefix: event.roleId,
125
176
  prefixColor: roleColor,
126
- text: `\u2717 ${error}`,
177
+ text: `✗ ${error}`,
127
178
  color: 'red',
179
+ indent: !isSupervisor,
128
180
  };
129
181
  }
130
182
 
131
- case 'msg:awaiting_input': {
183
+ case 'msg:awaiting_input':
132
184
  return {
133
185
  id: ++lineCounter,
134
- prefix: roleName,
135
- prefixColor: roleColor,
136
- text: '? Awaiting input...',
186
+ text: isSupervisor ? '...' : ` ${event.roleId}: waiting`,
137
187
  color: 'yellow',
138
188
  };
139
- }
140
189
 
141
- // Hidden events
190
+ // Hidden
142
191
  case 'thinking':
143
192
  case 'heartbeat:tick':
144
193
  case 'heartbeat:skip':
@@ -185,18 +234,17 @@ export const CommandMode: React.FC<CommandModeProps> = ({
185
234
  {allLines.length === 0 && (
186
235
  <Box marginTop={1}>
187
236
  <Text color="gray" dimColor>
188
- Ready. Type "wave &lt;directive&gt;" to start, or "help" for commands.
237
+ Type a message to your AI team, or /help for commands.
189
238
  </Text>
190
239
  </Box>
191
240
  )}
192
241
  {allLines.map((line) => (
193
242
  <Box key={line.id}>
243
+ {line.indent && <Text> </Text>}
194
244
  {line.prefix && (
195
- <>
196
- <Text color={line.prefixColor} bold>
197
- {(line.prefix + ':').padEnd(14)}
198
- </Text>
199
- </>
245
+ <Text color={line.prefixColor} bold>
246
+ {(line.prefix).padEnd(12)}
247
+ </Text>
200
248
  )}
201
249
  <Text color={line.color}>{line.text}</Text>
202
250
  </Box>
@@ -208,10 +256,10 @@ export const CommandMode: React.FC<CommandModeProps> = ({
208
256
  <Text color="gray">{'─'.repeat(process.stdout.columns || 70)}</Text>
209
257
  </Box>
210
258
 
211
- {/* Command input */}
259
+ {/* Input */}
212
260
  <Box paddingX={1} justifyContent="space-between">
213
261
  <Box>
214
- <Text color="green" bold>&gt; </Text>
262
+ <Text color="yellow" bold>&gt; </Text>
215
263
  <TextInput
216
264
  value={input}
217
265
  onChange={setInput}
@@ -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 status, active roles, cost
3
4
  */
4
5
 
5
6
  import React from 'react';
@@ -21,29 +22,33 @@ export const StatusBar: React.FC<StatusBarProps> = ({
21
22
  totalCost,
22
23
  }) => {
23
24
  const waveLabel = waveId
24
- ? `Wave ${waveId.replace('wave-', '#')} ${waveStatus === 'running' ? '\u25CF' : waveStatus === 'done' ? '\u2713' : ''} ${waveStatus}`
25
- : 'No active wave';
25
+ ? `Wave ${waveId.replace('wave-', '#')}`
26
+ : '';
27
+ const statusDot = waveStatus === 'running' ? ' ●'
28
+ : waveStatus === 'done' ? ' ✓'
29
+ : '';
26
30
 
27
31
  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>
32
+ <Box width="100%" paddingX={1}>
33
+ <Text color="cyan" bold>Tycono</Text>
34
+ <Text color="gray"> | </Text>
35
+ <Text color="white">{companyName}</Text>
36
+ {waveLabel && (
37
+ <>
38
+ <Text color="gray"> | </Text>
39
+ <Text color={waveStatus === 'running' ? 'green' : 'gray'}>
40
+ {waveLabel}{statusDot}
41
+ </Text>
42
+ </>
43
+ )}
44
+ {activeCount > 0 && (
45
+ <>
46
+ <Text color="gray"> | </Text>
47
+ <Text color="yellow">{activeCount} active</Text>
48
+ </>
49
+ )}
50
+ <Text color="gray"> | </Text>
51
+ <Text color="green">${totalCost.toFixed(2)}</Text>
47
52
  </Box>
48
53
  );
49
54
  };
@@ -1,54 +1,25 @@
1
1
  /**
2
- * useCommand — command parsing + execution for TUI v2 Command Mode
2
+ * useCommand — input handler for TUI v2
3
3
  *
4
- * Commands:
5
- * wave <directive> [--continuous] — dispatch a wave
6
- * directive <text> send directive to active wave
7
- * stop abort all active executions
8
- * status show current wave + session status
9
- * assign <role> <task> assign task to specific role
10
- * summary — show last wave summary
11
- * roles show org tree inline
12
- * help — show help
13
- * quit — exit
4
+ * Default: natural language → wave dispatch or directive (if wave active)
5
+ * Commands (/ prefix):
6
+ * /stop abort all active executions
7
+ * /status show current wave + session status
8
+ * /assign <role> <task> assign task to specific role
9
+ * /roles show org tree (Panel Mode)
10
+ * /help — show help
11
+ * /quit exit
14
12
  */
15
13
 
16
14
  import { useCallback } from 'react';
17
15
  import { dispatchWave, sendDirective, fetchJson } from '../api';
18
16
 
19
- export interface ParsedCommand {
20
- cmd: string;
21
- args: string;
22
- flags: Record<string, boolean>;
23
- }
24
-
25
17
  export interface CommandResult {
26
18
  type: 'success' | 'error' | 'info' | 'wave_started' | 'directive_sent' | 'stopped' | 'quit' | 'help' | 'panel';
27
19
  message: string;
28
20
  waveId?: string;
29
21
  }
30
22
 
31
- export function parseCommand(input: string): ParsedCommand {
32
- const trimmed = input.trim();
33
- if (!trimmed) return { cmd: '', args: '', flags: {} };
34
-
35
- const parts = trimmed.split(/\s+/);
36
- const cmd = parts[0].toLowerCase();
37
- const flags: Record<string, boolean> = {};
38
- const argParts: string[] = [];
39
-
40
- for (let i = 1; i < parts.length; i++) {
41
- if (parts[i].startsWith('--')) {
42
- const flagName = parts[i].slice(2);
43
- if (flagName) flags[flagName] = true;
44
- } else {
45
- argParts.push(parts[i]);
46
- }
47
- }
48
-
49
- return { cmd, args: argParts.join(' '), flags };
50
- }
51
-
52
23
  async function postAbortAll(): Promise<void> {
53
24
  try {
54
25
  const status = await fetchJson<{
@@ -88,97 +59,83 @@ export function useCommand(options: UseCommandOptions) {
88
59
  const { activeWaveId, onWaveStarted, onStopped, onQuit, onShowPanel } = options;
89
60
 
90
61
  const execute = useCallback(async (input: string): Promise<CommandResult> => {
91
- const { cmd, args, flags } = parseCommand(input);
92
-
93
- if (!cmd) return { type: 'info', message: '' };
94
-
95
- switch (cmd) {
96
- case 'wave': {
97
- if (!args) {
98
- return { type: 'error', message: 'Usage: wave <directive> [--continuous]' };
99
- }
100
- try {
101
- const result = await dispatchWave(args, { continuous: flags.continuous });
102
- onWaveStarted(result.waveId);
103
- const modeLabel = flags.continuous ? ' (continuous)' : '';
104
- return {
105
- type: 'wave_started',
106
- message: `Wave ${result.waveId.replace('wave-', '#')} started${modeLabel}`,
107
- waveId: result.waveId,
108
- };
109
- } catch (err) {
110
- return { type: 'error', message: `Wave failed: ${err instanceof Error ? err.message : 'unknown'}` };
111
- }
112
- }
113
-
114
- case 'directive': {
115
- if (!args) {
116
- return { type: 'error', message: 'Usage: directive <text>' };
117
- }
118
- if (!activeWaveId) {
119
- return { type: 'error', message: 'No active wave. Start one with: wave <directive>' };
120
- }
121
- try {
122
- await sendDirective(activeWaveId, args);
123
- return { type: 'directive_sent', message: `Directive sent to ${activeWaveId.replace('wave-', 'Wave #')}` };
124
- } catch (err) {
125
- return { type: 'error', message: `Directive failed: ${err instanceof Error ? err.message : 'unknown'}` };
126
- }
127
- }
128
-
129
- case 'stop': {
130
- try {
131
- await postAbortAll();
132
- onStopped();
133
- return { type: 'stopped', message: 'All active executions stopped.' };
134
- } catch (err) {
135
- return { type: 'error', message: `Stop failed: ${err instanceof Error ? err.message : 'unknown'}` };
62
+ const trimmed = input.trim();
63
+ if (!trimmed) return { type: 'info', message: '' };
64
+
65
+ // Slash commands: /stop, /status, /help, /quit, /roles, /assign
66
+ if (trimmed.startsWith('/')) {
67
+ const parts = trimmed.slice(1).split(/\s+/);
68
+ const cmd = parts[0].toLowerCase();
69
+ const args = parts.slice(1).join(' ');
70
+
71
+ switch (cmd) {
72
+ case 'stop': {
73
+ try {
74
+ await postAbortAll();
75
+ onStopped();
76
+ return { type: 'stopped', message: 'All active executions stopped.' };
77
+ } catch (err) {
78
+ return { type: 'error', message: `Stop failed: ${err instanceof Error ? err.message : 'unknown'}` };
79
+ }
136
80
  }
137
- }
138
81
 
139
- case 'status': {
140
- return { type: 'info', message: '__status__' };
141
- }
82
+ case 'status':
83
+ return { type: 'info', message: '__status__' };
142
84
 
143
- case 'assign': {
144
- const spaceIdx = args.indexOf(' ');
145
- if (spaceIdx === -1 || !args) {
146
- return { type: 'error', message: 'Usage: assign <role> <task>' };
147
- }
148
- const roleId = args.slice(0, spaceIdx);
149
- const task = args.slice(spaceIdx + 1);
150
- try {
151
- const result = await postAssign(roleId, task);
152
- if (result.waveId) {
153
- onWaveStarted(result.waveId);
85
+ case 'assign': {
86
+ const spaceIdx = args.indexOf(' ');
87
+ if (spaceIdx === -1 || !args) {
88
+ return { type: 'error', message: 'Usage: /assign <role> <task>' };
89
+ }
90
+ const roleId = args.slice(0, spaceIdx);
91
+ const task = args.slice(spaceIdx + 1);
92
+ try {
93
+ const result = await postAssign(roleId, task);
94
+ if (result.waveId) {
95
+ onWaveStarted(result.waveId);
96
+ }
97
+ return { type: 'success', message: `Task assigned to ${roleId}`, waveId: result.waveId };
98
+ } catch (err) {
99
+ return { type: 'error', message: `Assign failed: ${err instanceof Error ? err.message : 'unknown'}` };
154
100
  }
155
- return { type: 'success', message: `Task assigned to ${roleId}`, waveId: result.waveId };
156
- } catch (err) {
157
- return { type: 'error', message: `Assign failed: ${err instanceof Error ? err.message : 'unknown'}` };
158
101
  }
159
- }
160
102
 
161
- case 'summary': {
162
- return { type: 'info', message: '__summary__' };
163
- }
103
+ case 'roles':
104
+ onShowPanel();
105
+ return { type: 'panel', message: '' };
164
106
 
165
- case 'roles': {
166
- onShowPanel();
167
- return { type: 'panel', message: 'Switching to Panel Mode...' };
168
- }
107
+ case 'help':
108
+ return { type: 'help', message: '__help__' };
169
109
 
170
- case 'help': {
171
- return { type: 'help', message: '__help__' };
172
- }
110
+ case 'quit':
111
+ case 'exit':
112
+ onQuit();
113
+ return { type: 'quit', message: 'Goodbye!' };
173
114
 
174
- case 'quit':
175
- case 'exit': {
176
- onQuit();
177
- return { type: 'quit', message: 'Goodbye!' };
115
+ default:
116
+ return { type: 'error', message: `Unknown command: /${cmd}. Type /help for commands.` };
178
117
  }
118
+ }
179
119
 
180
- default: {
181
- return { type: 'error', message: `Unknown command: ${cmd}. Type "help" for available commands.` };
120
+ // Default: natural language → directive (if wave active) or new wave
121
+ if (activeWaveId) {
122
+ try {
123
+ await sendDirective(activeWaveId, trimmed);
124
+ return { type: 'directive_sent', message: `Directive sent` };
125
+ } catch (err) {
126
+ return { type: 'error', message: `Failed: ${err instanceof Error ? err.message : 'unknown'}` };
127
+ }
128
+ } else {
129
+ try {
130
+ const result = await dispatchWave(trimmed);
131
+ onWaveStarted(result.waveId);
132
+ return {
133
+ type: 'wave_started',
134
+ message: `Wave ${result.waveId.replace('wave-', '#')} started`,
135
+ waveId: result.waveId,
136
+ };
137
+ } catch (err) {
138
+ return { type: 'error', message: `Wave failed: ${err instanceof Error ? err.message : 'unknown'}` };
182
139
  }
183
140
  }
184
141
  }, [activeWaveId, onWaveStarted, onStopped, onQuit, onShowPanel]);