tycono 0.1.96-beta.7 → 0.1.96-beta.9
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 +16 -14
- package/package.json +1 -1
- package/src/tui/api.ts +1 -1
- package/src/tui/app.tsx +154 -154
- package/src/tui/components/CommandMode.tsx +228 -0
- package/src/tui/components/OrgTree.tsx +1 -1
- package/src/tui/components/PanelMode.tsx +112 -0
- package/src/tui/components/{StreamPanel.tsx → StreamView.tsx} +51 -73
- package/src/tui/hooks/useCommand.ts +187 -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
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<h1 align="center">tycono</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
9
|
-
<sub>
|
|
8
|
+
<strong>Cursor gives you one AI developer. Tycono gives you an AI team.</strong><br>
|
|
9
|
+
<sub>Give one order. Watch your AI team plan, build, and learn together.</sub>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -25,9 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Cursor, Lovable, Bolt — they all give you **one AI agent**. It helps, but you still drive everything.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
**tycono** gives you an **AI team**. A CTO reviews architecture. Engineers write code. A PM breaks down tasks. QA catches bugs. You just give the order and watch them work.
|
|
31
|
+
|
|
32
|
+
One command. Your AI team is running.
|
|
31
33
|
|
|
32
34
|
```bash
|
|
33
35
|
npx tycono
|
|
@@ -83,16 +85,16 @@ Session 50 is dramatically smarter than session 1. Your company learns.
|
|
|
83
85
|
|
|
84
86
|
## Why Tycono?
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
Same goal as Cursor, Lovable, Bolt — **get AI to do your work**. Different method.
|
|
87
89
|
|
|
88
|
-
| |
|
|
90
|
+
| | Cursor / Lovable / Bolt | Tycono |
|
|
89
91
|
|---|---|---|
|
|
90
|
-
| **
|
|
91
|
-
| **
|
|
92
|
-
| **
|
|
93
|
-
| **
|
|
94
|
-
| **Scale** | 1
|
|
95
|
-
| **Visibility** |
|
|
92
|
+
| **Agents** | 1 AI helps you | **AI team works for you** |
|
|
93
|
+
| **Your role** | Keep directing | **Give one order, watch** |
|
|
94
|
+
| **Knowledge** | Resets every session | **Compounds forever** |
|
|
95
|
+
| **Quality** | You review everything | **QA agent catches bugs** |
|
|
96
|
+
| **Scale** | 1 task at a time | **Parallel across roles** |
|
|
97
|
+
| **Visibility** | Editor / chat | **Real-time org tree** |
|
|
96
98
|
|
|
97
99
|
## Company-as-Code
|
|
98
100
|
|
|
@@ -258,9 +260,9 @@ npx tycono --version # Show version
|
|
|
258
260
|
- [x] CEO Wave dispatch with org-tree targeting
|
|
259
261
|
- [x] AKB — Pre-K / Post-K knowledge loop
|
|
260
262
|
- [x] Port Registry for multi-agent isolation
|
|
261
|
-
- [ ] **TUI mode** — terminal-native multi-panel interface
|
|
263
|
+
- [ ] **TUI mode** — terminal-native multi-panel interface *(in progress)*
|
|
262
264
|
- [ ] Git worktree isolation per agent session
|
|
263
|
-
- [ ] Desktop app (.dmg / .exe) — background execution,
|
|
265
|
+
- [ ] **Desktop app** (.dmg / .exe) — background execution, notifications, no API key setup needed
|
|
264
266
|
- [ ] Multi-LLM support (OpenAI, local models)
|
|
265
267
|
|
|
266
268
|
## Built with Tycono
|
package/package.json
CHANGED
package/src/tui/api.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function getBaseUrl(): string {
|
|
|
16
16
|
|
|
17
17
|
/* ─── HTTP helpers ─── */
|
|
18
18
|
|
|
19
|
-
async function fetchJson<T>(path: string, options?: { method?: string; body?: unknown }): Promise<T> {
|
|
19
|
+
export async function fetchJson<T>(path: string, options?: { method?: string; body?: unknown }): Promise<T> {
|
|
20
20
|
const url = `${BASE_URL}${path}`;
|
|
21
21
|
const method = options?.method ?? 'GET';
|
|
22
22
|
const bodyStr = options?.body ? JSON.stringify(options.body) : undefined;
|
package/src/tui/app.tsx
CHANGED
|
@@ -1,46 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TUI App —
|
|
2
|
+
* TUI App v2 — Hybrid Mode (Command + Panel)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* │ │ │
|
|
10
|
-
* ├──────────────┤ │
|
|
11
|
-
* │ SessionList │ │
|
|
12
|
-
* ├──────────────┴──────────────────────────┤
|
|
13
|
-
* │ CommandInput │
|
|
14
|
-
* └─────────────────────────────────────────┘
|
|
4
|
+
* Two modes:
|
|
5
|
+
* Command Mode (default) — stream summary + command input (> prompt)
|
|
6
|
+
* Panel Mode (Tab) — Org Tree left + Role stream right
|
|
7
|
+
*
|
|
8
|
+
* Tab toggles between modes, Esc returns to Command Mode.
|
|
15
9
|
*/
|
|
16
10
|
|
|
17
11
|
import React, { useState, useCallback, useMemo } from 'react';
|
|
18
|
-
import { Box, Text, useApp } from 'ink';
|
|
12
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
19
13
|
import { StatusBar } from './components/StatusBar';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { StreamPanel } from './components/StreamPanel';
|
|
23
|
-
import { CommandInput } from './components/CommandInput';
|
|
24
|
-
import { WaveDialog } from './components/WaveDialog';
|
|
25
|
-
import { HelpOverlay } from './components/HelpOverlay';
|
|
14
|
+
import { CommandMode, type StreamLine } from './components/CommandMode';
|
|
15
|
+
import { PanelMode } from './components/PanelMode';
|
|
26
16
|
import { SetupWizard } from './components/SetupWizard';
|
|
27
17
|
import { useApi } from './hooks/useApi';
|
|
28
18
|
import { useSSE } from './hooks/useSSE';
|
|
29
|
-
import {
|
|
19
|
+
import { useCommand } from './hooks/useCommand';
|
|
30
20
|
import { buildOrgTree } from './store';
|
|
31
|
-
import { dispatchWave } from './api';
|
|
32
21
|
|
|
33
|
-
type
|
|
34
|
-
type Dialog = 'none' | 'wave' | 'help';
|
|
22
|
+
type Mode = 'command' | 'panel';
|
|
35
23
|
type View = 'loading' | 'setup' | 'dashboard';
|
|
36
24
|
|
|
37
|
-
|
|
25
|
+
let sysLineId = 100000;
|
|
38
26
|
|
|
39
27
|
export const App: React.FC = () => {
|
|
40
28
|
const { exit } = useApp();
|
|
41
29
|
const api = useApi();
|
|
42
30
|
|
|
43
|
-
//
|
|
31
|
+
// View state: loading -> setup (no company) -> dashboard
|
|
44
32
|
const [view, setView] = useState<View>('loading');
|
|
45
33
|
|
|
46
34
|
React.useEffect(() => {
|
|
@@ -55,16 +43,31 @@ export const App: React.FC = () => {
|
|
|
55
43
|
setView('dashboard');
|
|
56
44
|
}, [api]);
|
|
57
45
|
|
|
58
|
-
|
|
59
|
-
const [
|
|
60
|
-
|
|
61
|
-
|
|
46
|
+
// Mode state
|
|
47
|
+
const [mode, setMode] = useState<Mode>('command');
|
|
48
|
+
|
|
49
|
+
// Wave state
|
|
62
50
|
const [waveId, setWaveId] = useState<string | null>(null);
|
|
63
51
|
const [waveStatus, setWaveStatus] = useState<'idle' | 'running' | 'done'>('idle');
|
|
64
52
|
|
|
65
|
-
//
|
|
53
|
+
// Panel mode state
|
|
54
|
+
const [selectedRoleIndex, setSelectedRoleIndex] = useState(0);
|
|
55
|
+
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
// System messages (command feedback displayed in stream area)
|
|
58
|
+
const [systemMessages, setSystemMessages] = useState<StreamLine[]>([]);
|
|
59
|
+
|
|
60
|
+
const addSystemMessage = useCallback((text: string, color: string = 'yellow') => {
|
|
61
|
+
setSystemMessages(prev => {
|
|
62
|
+
const next = [...prev, { id: ++sysLineId, text, color }];
|
|
63
|
+
return next.length > 50 ? next.slice(-50) : next;
|
|
64
|
+
});
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Derive effective wave ID (from manual set or API polling)
|
|
66
68
|
const effectiveWaveId = waveId ?? api.activeWaveId;
|
|
67
69
|
|
|
70
|
+
// SSE subscription
|
|
68
71
|
const sse = useSSE(effectiveWaveId);
|
|
69
72
|
|
|
70
73
|
// Build org tree
|
|
@@ -73,10 +76,12 @@ export const App: React.FC = () => {
|
|
|
73
76
|
const statuses = api.execStatus?.statuses ?? {};
|
|
74
77
|
const orgTree = useMemo(() => buildOrgTree(roles, statuses), [roles, statuses]);
|
|
75
78
|
|
|
76
|
-
//
|
|
77
|
-
const activeCount = Object.values(statuses).filter(
|
|
79
|
+
// Active count
|
|
80
|
+
const activeCount = Object.values(statuses).filter(
|
|
81
|
+
s => s === 'working' || s === 'streaming'
|
|
82
|
+
).length;
|
|
78
83
|
|
|
79
|
-
//
|
|
84
|
+
// Derived wave status
|
|
80
85
|
const derivedWaveStatus = useMemo(() => {
|
|
81
86
|
if (sse.streamStatus === 'streaming') return 'running' as const;
|
|
82
87
|
if (sse.streamStatus === 'done') return 'done' as const;
|
|
@@ -84,55 +89,94 @@ export const App: React.FC = () => {
|
|
|
84
89
|
return waveStatus;
|
|
85
90
|
}, [sse.streamStatus, waveStatus, activeCount]);
|
|
86
91
|
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
setWaveId(result.waveId);
|
|
92
|
+
// Command handler
|
|
93
|
+
const { execute } = useCommand({
|
|
94
|
+
activeWaveId: effectiveWaveId,
|
|
95
|
+
onWaveStarted: (newWaveId) => {
|
|
96
|
+
setWaveId(newWaveId);
|
|
93
97
|
setWaveStatus('running');
|
|
94
98
|
sse.clearEvents();
|
|
95
99
|
api.refresh();
|
|
96
|
-
} catch (err) {
|
|
97
|
-
// Show error briefly
|
|
98
|
-
console.error('Wave dispatch failed:', err);
|
|
99
|
-
}
|
|
100
|
-
}, [sse, api]);
|
|
101
|
-
|
|
102
|
-
// Keyboard actions — disabled when dialog is open
|
|
103
|
-
const keyboardEnabled = dialog === 'none';
|
|
104
|
-
|
|
105
|
-
useKeyboard({
|
|
106
|
-
onWave: () => setDialog('wave'),
|
|
107
|
-
onQuit: () => exit(),
|
|
108
|
-
onHelp: () => setDialog(dialog === 'help' ? 'none' : 'help'),
|
|
109
|
-
onTab: () => {
|
|
110
|
-
const idx = PANELS.indexOf(activePanel);
|
|
111
|
-
setActivePanel(PANELS[(idx + 1) % PANELS.length]);
|
|
112
|
-
},
|
|
113
|
-
onUp: () => {
|
|
114
|
-
if (activePanel === 'org') {
|
|
115
|
-
setSelectedRoleIndex(Math.max(0, selectedRoleIndex - 1));
|
|
116
|
-
} else if (activePanel === 'sessions') {
|
|
117
|
-
setSelectedSessionIndex(Math.max(0, selectedSessionIndex - 1));
|
|
118
|
-
}
|
|
119
100
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
} else if (activePanel === 'sessions') {
|
|
124
|
-
setSelectedSessionIndex(Math.min(Math.max(0, api.sessions.length - 1), selectedSessionIndex + 1));
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
onEnter: () => {
|
|
128
|
-
// Future: select role/session to show in stream
|
|
101
|
+
onStopped: () => {
|
|
102
|
+
setWaveStatus('idle');
|
|
103
|
+
api.refresh();
|
|
129
104
|
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
105
|
+
onQuit: () => exit(),
|
|
106
|
+
onShowPanel: () => setMode('panel'),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Handle command submission from CommandMode
|
|
110
|
+
const handleCommandSubmit = useCallback(async (input: string) => {
|
|
111
|
+
addSystemMessage(`> ${input}`, 'white');
|
|
112
|
+
|
|
113
|
+
const result = await execute(input);
|
|
114
|
+
|
|
115
|
+
switch (result.type) {
|
|
116
|
+
case 'wave_started':
|
|
117
|
+
addSystemMessage(`\u26A1 ${result.message}`, 'yellow');
|
|
118
|
+
break;
|
|
119
|
+
case 'directive_sent':
|
|
120
|
+
addSystemMessage(`\u26A1 ${result.message}`, 'yellow');
|
|
121
|
+
break;
|
|
122
|
+
case 'stopped':
|
|
123
|
+
addSystemMessage(`\u26A1 ${result.message}`, 'red');
|
|
124
|
+
break;
|
|
125
|
+
case 'error':
|
|
126
|
+
addSystemMessage(result.message, 'red');
|
|
127
|
+
break;
|
|
128
|
+
case 'help':
|
|
129
|
+
addSystemMessage('Commands:', 'cyan');
|
|
130
|
+
addSystemMessage(' wave <directive> [--continuous] Start a wave', 'white');
|
|
131
|
+
addSystemMessage(' directive <text> Send directive to active wave', 'white');
|
|
132
|
+
addSystemMessage(' stop Stop all active executions', 'white');
|
|
133
|
+
addSystemMessage(' status Show current status', 'white');
|
|
134
|
+
addSystemMessage(' assign <role> <task> Assign task to role', 'white');
|
|
135
|
+
addSystemMessage(' summary Show last wave summary', 'white');
|
|
136
|
+
addSystemMessage(' roles Show org tree (Panel Mode)', 'white');
|
|
137
|
+
addSystemMessage(' help Show this help', 'white');
|
|
138
|
+
addSystemMessage(' quit Exit TUI', 'white');
|
|
139
|
+
addSystemMessage('Keys: [Tab] panel [Esc] back [Ctrl+C] stop/quit', 'gray');
|
|
140
|
+
break;
|
|
141
|
+
case 'info':
|
|
142
|
+
if (result.message === '__status__') {
|
|
143
|
+
const wLabel = effectiveWaveId
|
|
144
|
+
? `Wave ${effectiveWaveId.replace('wave-', '#')}: ${derivedWaveStatus}`
|
|
145
|
+
: 'No active wave';
|
|
146
|
+
addSystemMessage(wLabel, derivedWaveStatus === 'running' ? 'green' : 'gray');
|
|
147
|
+
addSystemMessage(`Sessions: ${api.sessions.length} Active: ${activeCount}`, 'white');
|
|
148
|
+
} else if (result.message === '__summary__') {
|
|
149
|
+
addSystemMessage('Summary: not yet implemented', 'gray');
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
case 'panel':
|
|
153
|
+
// mode already switched
|
|
154
|
+
break;
|
|
155
|
+
case 'quit':
|
|
156
|
+
// exit already called
|
|
157
|
+
break;
|
|
158
|
+
default:
|
|
159
|
+
if (result.message) {
|
|
160
|
+
addSystemMessage(result.message, 'green');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}, [execute, addSystemMessage, effectiveWaveId, derivedWaveStatus, api.sessions.length, activeCount]);
|
|
164
|
+
|
|
165
|
+
// Global key handler: Tab to toggle mode, Ctrl+C handling
|
|
166
|
+
useInput((input, key) => {
|
|
167
|
+
if (mode === 'command' && key.tab) {
|
|
168
|
+
setMode('panel');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Ctrl+C in command mode: stop wave or exit
|
|
172
|
+
if (key.ctrl && input === 'c') {
|
|
173
|
+
if (derivedWaveStatus === 'running') {
|
|
174
|
+
execute('stop');
|
|
175
|
+
} else {
|
|
176
|
+
exit();
|
|
133
177
|
}
|
|
134
|
-
}
|
|
135
|
-
},
|
|
178
|
+
}
|
|
179
|
+
}, { isActive: mode === 'command' });
|
|
136
180
|
|
|
137
181
|
// Loading state
|
|
138
182
|
if (view === 'loading') {
|
|
@@ -144,7 +188,7 @@ export const App: React.FC = () => {
|
|
|
144
188
|
);
|
|
145
189
|
}
|
|
146
190
|
|
|
147
|
-
// Setup wizard
|
|
191
|
+
// Setup wizard
|
|
148
192
|
if (view === 'setup') {
|
|
149
193
|
return (
|
|
150
194
|
<Box flexDirection="column" paddingX={1}>
|
|
@@ -160,49 +204,17 @@ export const App: React.FC = () => {
|
|
|
160
204
|
<Text color="cyan" bold>TYCONO TUI</Text>
|
|
161
205
|
<Text color="red">API Error: {api.error}</Text>
|
|
162
206
|
<Text color="gray">Make sure the API server is running on the configured port.</Text>
|
|
163
|
-
<Text color="gray" dimColor>Press
|
|
207
|
+
<Text color="gray" dimColor>Press Ctrl+C to quit</Text>
|
|
164
208
|
</Box>
|
|
165
209
|
);
|
|
166
210
|
}
|
|
167
211
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<Box flexDirection="column">
|
|
172
|
-
<StatusBar
|
|
173
|
-
companyName={api.company?.name ?? 'Loading...'}
|
|
174
|
-
waveId={effectiveWaveId}
|
|
175
|
-
waveStatus={derivedWaveStatus}
|
|
176
|
-
activeCount={activeCount}
|
|
177
|
-
totalCost={0}
|
|
178
|
-
/>
|
|
179
|
-
<HelpOverlay onClose={() => setDialog('none')} />
|
|
180
|
-
</Box>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Wave dialog
|
|
185
|
-
if (dialog === 'wave') {
|
|
186
|
-
return (
|
|
187
|
-
<Box flexDirection="column">
|
|
188
|
-
<StatusBar
|
|
189
|
-
companyName={api.company?.name ?? 'Loading...'}
|
|
190
|
-
waveId={effectiveWaveId}
|
|
191
|
-
waveStatus={derivedWaveStatus}
|
|
192
|
-
activeCount={activeCount}
|
|
193
|
-
totalCost={0}
|
|
194
|
-
/>
|
|
195
|
-
<WaveDialog
|
|
196
|
-
onSubmit={handleWaveSubmit}
|
|
197
|
-
onCancel={() => setDialog('none')}
|
|
198
|
-
/>
|
|
199
|
-
</Box>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
212
|
+
// Terminal full height
|
|
213
|
+
const termHeight = process.stdout.rows || 30;
|
|
202
214
|
|
|
203
215
|
return (
|
|
204
|
-
<Box flexDirection="column">
|
|
205
|
-
{/* Status Bar */}
|
|
216
|
+
<Box flexDirection="column" height={termHeight}>
|
|
217
|
+
{/* Status Bar — always shown */}
|
|
206
218
|
<StatusBar
|
|
207
219
|
companyName={api.company?.name ?? 'Loading...'}
|
|
208
220
|
waveId={effectiveWaveId}
|
|
@@ -213,54 +225,42 @@ export const App: React.FC = () => {
|
|
|
213
225
|
|
|
214
226
|
{/* Separator */}
|
|
215
227
|
<Box width="100%">
|
|
216
|
-
<Text color="gray">{'
|
|
228
|
+
<Text color="gray">{'─'.repeat(process.stdout.columns || 70)}</Text>
|
|
217
229
|
</Box>
|
|
218
230
|
|
|
219
|
-
{/*
|
|
220
|
-
<Box flexGrow={1}>
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
<OrgTree
|
|
224
|
-
tree={orgTree}
|
|
225
|
-
focused={activePanel === 'org'}
|
|
226
|
-
selectedIndex={selectedRoleIndex}
|
|
227
|
-
flatRoles={flatRoleIds}
|
|
228
|
-
/>
|
|
229
|
-
<Box marginTop={1}>
|
|
230
|
-
<SessionList
|
|
231
|
-
sessions={api.sessions}
|
|
232
|
-
focused={activePanel === 'sessions'}
|
|
233
|
-
selectedIndex={selectedSessionIndex}
|
|
234
|
-
/>
|
|
235
|
-
</Box>
|
|
236
|
-
</Box>
|
|
237
|
-
|
|
238
|
-
{/* Vertical separator */}
|
|
239
|
-
<Box flexDirection="column" marginX={0}>
|
|
240
|
-
<Text color="gray">{'\u2502\n'.repeat(15)}</Text>
|
|
241
|
-
</Box>
|
|
242
|
-
|
|
243
|
-
{/* Right column: Stream */}
|
|
244
|
-
<StreamPanel
|
|
231
|
+
{/* Mode content — fill remaining height */}
|
|
232
|
+
<Box flexGrow={1} flexDirection="column">
|
|
233
|
+
{mode === 'command' ? (
|
|
234
|
+
<CommandMode
|
|
245
235
|
events={sse.events}
|
|
246
236
|
allRoleIds={flatRoleIds}
|
|
247
|
-
|
|
237
|
+
systemMessages={systemMessages}
|
|
238
|
+
onSubmit={handleCommandSubmit}
|
|
239
|
+
/>
|
|
240
|
+
) : (
|
|
241
|
+
<PanelMode
|
|
242
|
+
tree={orgTree}
|
|
243
|
+
flatRoles={flatRoleIds}
|
|
244
|
+
events={sse.events}
|
|
245
|
+
selectedRoleIndex={selectedRoleIndex}
|
|
246
|
+
selectedRoleId={selectedRoleId}
|
|
248
247
|
streamStatus={sse.streamStatus}
|
|
249
248
|
waveId={effectiveWaveId}
|
|
249
|
+
onMove={(dir) => {
|
|
250
|
+
if (dir === 'up') {
|
|
251
|
+
setSelectedRoleIndex(Math.max(0, selectedRoleIndex - 1));
|
|
252
|
+
} else {
|
|
253
|
+
setSelectedRoleIndex(Math.min(flatRoleIds.length - 1, selectedRoleIndex + 1));
|
|
254
|
+
}
|
|
255
|
+
}}
|
|
256
|
+
onSelect={() => {
|
|
257
|
+
const roleId = flatRoleIds[selectedRoleIndex] ?? null;
|
|
258
|
+
setSelectedRoleId(roleId === selectedRoleId ? null : roleId);
|
|
259
|
+
}}
|
|
260
|
+
onEscape={() => setMode('command')}
|
|
250
261
|
/>
|
|
262
|
+
)}
|
|
251
263
|
</Box>
|
|
252
|
-
|
|
253
|
-
{/* Separator */}
|
|
254
|
-
<Box width="100%">
|
|
255
|
-
<Text color="gray">{'\u2500'.repeat(70)}</Text>
|
|
256
|
-
</Box>
|
|
257
|
-
|
|
258
|
-
{/* Command Input */}
|
|
259
|
-
<CommandInput
|
|
260
|
-
focused={activePanel === 'command'}
|
|
261
|
-
waveStatus={derivedWaveStatus}
|
|
262
|
-
dialog={dialog}
|
|
263
|
-
/>
|
|
264
264
|
</Box>
|
|
265
265
|
);
|
|
266
266
|
};
|