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 CHANGED
@@ -5,8 +5,8 @@
5
5
  <h1 align="center">tycono</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Build an AI company. Watch them work.</strong><br>
9
- <sub>Infrastructure-as-Code defined servers. Company-as-Code defines organizations.</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
- **tycono** is an open-source platform that lets you define and run an AI-powered organization. Roles, authority, knowledge, and workflows all defined in files, executed by AI agents, visualized in real time.
28
+ Cursor, Lovable, Bolt they all give you **one AI agent**. It helps, but you still drive everything.
29
29
 
30
- One command. Your AI company is running.
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
- Coding agents simulate **one developer**. Tycono simulates **the entire company**.
88
+ Same goal as Cursor, Lovable, Bolt **get AI to do your work**. Different method.
87
89
 
88
- | | Single AI Agent | Tycono |
90
+ | | Cursor / Lovable / Bolt | Tycono |
89
91
  |---|---|---|
90
- | **What it runs** | One agent, one context | Multiple roles with org hierarchy |
91
- | **Knowledge** | Resets every session | Compounds forever (AKB Pre-K/Post-K) |
92
- | **Authority** | Can do anything (or nothing) | Scoped each role has clear boundaries |
93
- | **Delegation** | Manual prompt chaining | CEO dispatches, org chart routes automatically |
94
- | **Scale** | 1 agent | 7 700 agents |
95
- | **Visibility** | Terminal output | Real-time org tree + activity stream |
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, system notifications
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.96-beta.7",
3
+ "version": "0.1.96-beta.8",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
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 — main layout with 4 panels
2
+ * TUI App v2 Hybrid Mode (Command + Panel)
3
3
  *
4
- * Layout:
5
- * ┌─────────────────────────────────────────┐
6
- * StatusBar │
7
- * ├──────────────┬──────────────────────────┤
8
- * OrgTree │ StreamPanel │
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 { OrgTree } from './components/OrgTree';
21
- import { SessionList } from './components/SessionList';
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 { useKeyboard } from './hooks/useKeyboard';
19
+ import { useCommand } from './hooks/useCommand';
30
20
  import { buildOrgTree } from './store';
31
- import { dispatchWave } from './api';
32
21
 
33
- type Panel = 'org' | 'sessions' | 'stream' | 'command';
34
- type Dialog = 'none' | 'wave' | 'help';
22
+ type Mode = 'command' | 'panel';
35
23
  type View = 'loading' | 'setup' | 'dashboard';
36
24
 
37
- const PANELS: Panel[] = ['org', 'sessions', 'stream', 'command'];
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
- // Determine view: loading setup (no company) dashboard
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
- const [activePanel, setActivePanel] = useState<Panel>('org');
59
- const [dialog, setDialog] = useState<Dialog>('none');
60
- const [selectedRoleIndex, setSelectedRoleIndex] = useState(0);
61
- const [selectedSessionIndex, setSelectedSessionIndex] = useState(0);
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
- // Derive active wave from API if we don't have one
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
- // Count active
77
- const activeCount = Object.values(statuses).filter(s => s === 'working' || s === 'streaming').length;
79
+ // Active count
80
+ const activeCount = Object.values(statuses).filter(
81
+ s => s === 'working' || s === 'streaming'
82
+ ).length;
78
83
 
79
- // Determine wave status from SSE
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
- // Handle wave dispatch
88
- const handleWaveSubmit = useCallback(async (directive: string) => {
89
- setDialog('none');
90
- try {
91
- const result = await dispatchWave(directive);
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
- 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
- },
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
- onEscape: () => {
131
- if (dialog !== 'none') {
132
- setDialog('none');
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
- }, keyboardEnabled);
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 — no company found
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 q to quit</Text>
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
- {/* Main content: left (org + sessions) | right (stream) */}
220
- <Box flexGrow={1}>
221
- {/* Left column */}
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
- focused={activePanel === 'stream'}
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
- </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
- />
258
+ )}
264
259
  </Box>
265
260
  );
266
261
  };