tycono 0.1.96-beta.12 → 0.1.96-beta.14
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 +20 -27
- package/src/tui/components/CommandMode.tsx +2 -2
- package/src/tui/components/OrgTree.tsx +1 -14
- package/src/tui/components/StatusBar.tsx +27 -22
- package/src/tui/hooks/useCommand.ts +75 -118
- package/src/tui/store.ts +12 -0
package/package.json
CHANGED
package/src/tui/app.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { SetupWizard } from './components/SetupWizard';
|
|
|
17
17
|
import { useApi } from './hooks/useApi';
|
|
18
18
|
import { useSSE } from './hooks/useSSE';
|
|
19
19
|
import { useCommand } from './hooks/useCommand';
|
|
20
|
-
import { buildOrgTree } from './store';
|
|
20
|
+
import { buildOrgTree, flattenOrgRoleIds } from './store';
|
|
21
21
|
|
|
22
22
|
type Mode = 'command' | 'panel';
|
|
23
23
|
type View = 'loading' | 'setup' | 'dashboard';
|
|
@@ -90,11 +90,11 @@ export const App: React.FC = () => {
|
|
|
90
90
|
// SSE subscription
|
|
91
91
|
const sse = useSSE(effectiveWaveId);
|
|
92
92
|
|
|
93
|
-
// Build org tree
|
|
93
|
+
// Build org tree — flatRoleIds follows visual top-to-bottom order
|
|
94
94
|
const roles = api.company?.roles ?? [];
|
|
95
|
-
const flatRoleIds = useMemo(() => roles.map(r => r.id), [roles]);
|
|
96
95
|
const statuses = api.execStatus?.statuses ?? {};
|
|
97
96
|
const orgTree = useMemo(() => buildOrgTree(roles, statuses), [roles, statuses]);
|
|
97
|
+
const flatRoleIds = useMemo(() => flattenOrgRoleIds(orgTree), [orgTree]);
|
|
98
98
|
|
|
99
99
|
// Active count
|
|
100
100
|
const activeCount = Object.values(statuses).filter(
|
|
@@ -146,16 +146,14 @@ export const App: React.FC = () => {
|
|
|
146
146
|
addSystemMessage(result.message, 'red');
|
|
147
147
|
break;
|
|
148
148
|
case 'help':
|
|
149
|
-
addSystemMessage('
|
|
150
|
-
addSystemMessage('
|
|
151
|
-
addSystemMessage('
|
|
152
|
-
addSystemMessage('
|
|
153
|
-
addSystemMessage('
|
|
154
|
-
addSystemMessage('
|
|
155
|
-
addSystemMessage('
|
|
156
|
-
addSystemMessage('
|
|
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
|
};
|
|
@@ -185,7 +185,7 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
185
185
|
{allLines.length === 0 && (
|
|
186
186
|
<Box marginTop={1}>
|
|
187
187
|
<Text color="gray" dimColor>
|
|
188
|
-
|
|
188
|
+
Type a message to your AI team, or /help for commands.
|
|
189
189
|
</Text>
|
|
190
190
|
</Box>
|
|
191
191
|
)}
|
|
@@ -211,7 +211,7 @@ export const CommandMode: React.FC<CommandModeProps> = ({
|
|
|
211
211
|
{/* Command input */}
|
|
212
212
|
<Box paddingX={1} justifyContent="space-between">
|
|
213
213
|
<Box>
|
|
214
|
-
<Text color="
|
|
214
|
+
<Text color="yellow" bold>> </Text>
|
|
215
215
|
<TextInput
|
|
216
216
|
value={input}
|
|
217
217
|
onChange={setInput}
|
|
@@ -32,7 +32,6 @@ function statusColor(status: string): string {
|
|
|
32
32
|
|
|
33
33
|
interface FlatEntry {
|
|
34
34
|
roleId: string;
|
|
35
|
-
name: string;
|
|
36
35
|
level: string;
|
|
37
36
|
status: string;
|
|
38
37
|
prefix: string;
|
|
@@ -55,7 +54,6 @@ function flattenTree(nodes: OrgNode[], prefix: string = '', isLast: boolean[] =
|
|
|
55
54
|
|
|
56
55
|
result.push({
|
|
57
56
|
roleId: node.role.id,
|
|
58
|
-
name: node.role.name || node.role.id,
|
|
59
57
|
level: node.role.level,
|
|
60
58
|
status: node.status,
|
|
61
59
|
prefix: linePrefix,
|
|
@@ -72,16 +70,6 @@ function flattenTree(nodes: OrgNode[], prefix: string = '', isLast: boolean[] =
|
|
|
72
70
|
export const OrgTree: React.FC<OrgTreeProps> = ({ tree, focused, selectedIndex, flatRoles }) => {
|
|
73
71
|
const entries = flattenTree(tree);
|
|
74
72
|
|
|
75
|
-
// Map flatRoles index to entries
|
|
76
|
-
const flatRoleIdToEntryIdx = new Map<number, number>();
|
|
77
|
-
let roleIdx = 0;
|
|
78
|
-
for (let i = 0; i < entries.length; i++) {
|
|
79
|
-
if (roleIdx < flatRoles.length && flatRoles[roleIdx] === entries[i].roleId) {
|
|
80
|
-
flatRoleIdToEntryIdx.set(roleIdx, i);
|
|
81
|
-
roleIdx++;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
73
|
return (
|
|
86
74
|
<Box flexDirection="column" paddingX={1}>
|
|
87
75
|
<Text bold color={focused ? 'cyan' : 'gray'}>{'── Org Tree ──'}</Text>
|
|
@@ -108,9 +96,8 @@ export const OrgTree: React.FC<OrgTreeProps> = ({ tree, focused, selectedIndex,
|
|
|
108
96
|
bold={isSelected}
|
|
109
97
|
inverse={isSelected}
|
|
110
98
|
>
|
|
111
|
-
{entry.
|
|
99
|
+
{entry.roleId}
|
|
112
100
|
</Text>
|
|
113
|
-
<Text color="gray" dimColor> {entry.roleId}</Text>
|
|
114
101
|
</Box>
|
|
115
102
|
);
|
|
116
103
|
})}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StatusBar —
|
|
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-', '#')}
|
|
25
|
-
: '
|
|
25
|
+
? `Wave ${waveId.replace('wave-', '#')}`
|
|
26
|
+
: '';
|
|
27
|
+
const statusDot = waveStatus === 'running' ? ' ●'
|
|
28
|
+
: waveStatus === 'done' ? ' ✓'
|
|
29
|
+
: '';
|
|
26
30
|
|
|
27
31
|
return (
|
|
28
|
-
<Box
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</
|
|
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 —
|
|
2
|
+
* useCommand — input handler for TUI v2
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
type: '
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
82
|
+
case 'status':
|
|
83
|
+
return { type: 'info', message: '__status__' };
|
|
142
84
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
103
|
+
case 'roles':
|
|
104
|
+
onShowPanel();
|
|
105
|
+
return { type: 'panel', message: '' };
|
|
164
106
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return { type: 'panel', message: 'Switching to Panel Mode...' };
|
|
168
|
-
}
|
|
107
|
+
case 'help':
|
|
108
|
+
return { type: 'help', message: '__help__' };
|
|
169
109
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
110
|
+
case 'quit':
|
|
111
|
+
case 'exit':
|
|
112
|
+
onQuit();
|
|
113
|
+
return { type: 'quit', message: 'Goodbye!' };
|
|
173
114
|
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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]);
|
package/src/tui/store.ts
CHANGED
|
@@ -97,3 +97,15 @@ export function buildOrgTree(roles: RoleInfo[], statuses: Record<string, string>
|
|
|
97
97
|
|
|
98
98
|
return roots;
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/** Flatten org tree into visual top-to-bottom order of role IDs */
|
|
102
|
+
export function flattenOrgRoleIds(nodes: OrgNode[]): string[] {
|
|
103
|
+
const result: string[] = [];
|
|
104
|
+
for (const node of nodes) {
|
|
105
|
+
result.push(node.role.id);
|
|
106
|
+
if (node.children.length > 0) {
|
|
107
|
+
result.push(...flattenOrgRoleIds(node.children));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|