tycono 0.1.96-beta.7 → 0.1.96-beta.8
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 +149 -154
- package/src/tui/components/CommandMode.tsx +228 -0
- 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
100
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else if (activePanel === 'sessions') {
|
|
117
|
-
setSelectedSessionIndex(Math.max(0, selectedSessionIndex - 1));
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
onDown: () => {
|
|
121
|
-
if (activePanel === 'org') {
|
|
122
|
-
setSelectedRoleIndex(Math.min(flatRoleIds.length - 1, selectedRoleIndex + 1));
|
|
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,14 @@ 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
|
|
164
|
-
</Box>
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Help overlay
|
|
169
|
-
if (dialog === 'help') {
|
|
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
|
-
/>
|
|
207
|
+
<Text color="gray" dimColor>Press Ctrl+C to quit</Text>
|
|
199
208
|
</Box>
|
|
200
209
|
);
|
|
201
210
|
}
|
|
202
211
|
|
|
203
212
|
return (
|
|
204
213
|
<Box flexDirection="column">
|
|
205
|
-
{/* Status Bar */}
|
|
214
|
+
{/* Status Bar — always shown */}
|
|
206
215
|
<StatusBar
|
|
207
216
|
companyName={api.company?.name ?? 'Loading...'}
|
|
208
217
|
waveId={effectiveWaveId}
|
|
@@ -216,51 +225,37 @@ export const App: React.FC = () => {
|
|
|
216
225
|
<Text color="gray">{'\u2500'.repeat(70)}</Text>
|
|
217
226
|
</Box>
|
|
218
227
|
|
|
219
|
-
{/*
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<Box flexDirection="column" width={28}>
|
|
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
|
|
228
|
+
{/* Mode content */}
|
|
229
|
+
{mode === 'command' ? (
|
|
230
|
+
<CommandMode
|
|
245
231
|
events={sse.events}
|
|
246
232
|
allRoleIds={flatRoleIds}
|
|
247
|
-
|
|
233
|
+
systemMessages={systemMessages}
|
|
234
|
+
onSubmit={handleCommandSubmit}
|
|
235
|
+
/>
|
|
236
|
+
) : (
|
|
237
|
+
<PanelMode
|
|
238
|
+
tree={orgTree}
|
|
239
|
+
flatRoles={flatRoleIds}
|
|
240
|
+
events={sse.events}
|
|
241
|
+
selectedRoleIndex={selectedRoleIndex}
|
|
242
|
+
selectedRoleId={selectedRoleId}
|
|
248
243
|
streamStatus={sse.streamStatus}
|
|
249
244
|
waveId={effectiveWaveId}
|
|
245
|
+
onMove={(dir) => {
|
|
246
|
+
if (dir === 'up') {
|
|
247
|
+
setSelectedRoleIndex(Math.max(0, selectedRoleIndex - 1));
|
|
248
|
+
} else {
|
|
249
|
+
setSelectedRoleIndex(Math.min(flatRoleIds.length - 1, selectedRoleIndex + 1));
|
|
250
|
+
}
|
|
251
|
+
}}
|
|
252
|
+
onSelect={() => {
|
|
253
|
+
const roleId = flatRoleIds[selectedRoleIndex] ?? null;
|
|
254
|
+
setSelectedRoleId(roleId === selectedRoleId ? null : roleId);
|
|
255
|
+
}}
|
|
256
|
+
onEscape={() => setMode('command')}
|
|
250
257
|
/>
|
|
251
|
-
|
|
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
|
-
/>
|
|
258
|
+
)}
|
|
264
259
|
</Box>
|
|
265
260
|
);
|
|
266
261
|
};
|