tycono 0.3.13 → 0.3.14-beta.0
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/api/src/engine/org-tree.ts +65 -50
- package/src/api/src/routes/execute.ts +2 -0
- package/src/api/src/services/scaffold.ts +1 -1
- package/src/api/src/services/supervisor-heartbeat.ts +10 -6
- package/src/api/src/services/wave-tracker.ts +11 -1
- package/src/tui/app.tsx +99 -13
- package/src/tui/components/CommandMode.tsx +1 -0
- package/src/tui/components/PanelMode.tsx +2 -2
- package/src/tui/components/StreamView.tsx +3 -3
package/package.json
CHANGED
|
@@ -85,7 +85,7 @@ interface RawRoleYaml {
|
|
|
85
85
|
|
|
86
86
|
/* ─── Build ──────────────────────────────────── */
|
|
87
87
|
|
|
88
|
-
export function buildOrgTree(companyRoot: string): OrgTree {
|
|
88
|
+
export function buildOrgTree(companyRoot: string, presetId?: string): OrgTree {
|
|
89
89
|
const rolesDir = path.join(companyRoot, 'roles');
|
|
90
90
|
const tree: OrgTree = { root: 'ceo', nodes: new Map() };
|
|
91
91
|
|
|
@@ -102,53 +102,68 @@ export function buildOrgTree(companyRoot: string): OrgTree {
|
|
|
102
102
|
reports: { daily: '', weekly: '' },
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
105
|
+
// Collect role directories to scan: base roles/ + preset roles/
|
|
106
|
+
const roleDirs: string[] = [];
|
|
107
|
+
if (fs.existsSync(rolesDir)) roleDirs.push(rolesDir);
|
|
108
|
+
|
|
109
|
+
// If preset specified, also scan preset's roles directory
|
|
110
|
+
if (presetId && presetId !== 'default') {
|
|
111
|
+
const presetRolesDir = path.join(companyRoot, 'company', 'presets', presetId, 'roles');
|
|
112
|
+
if (fs.existsSync(presetRolesDir)) roleDirs.push(presetRolesDir);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Read all role.yaml files from all role directories
|
|
116
|
+
for (const dir of roleDirs) {
|
|
117
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!entry.isDirectory()) continue;
|
|
120
|
+
const yamlPath = path.join(dir, entry.name, 'role.yaml');
|
|
121
|
+
if (!fs.existsSync(yamlPath)) continue;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const raw = YAML.parse(fs.readFileSync(yamlPath, 'utf-8')) as RawRoleYaml;
|
|
125
|
+
const nodeId = raw.id || entry.name;
|
|
126
|
+
|
|
127
|
+
// Skip if already loaded (base roles take precedence over preset roles)
|
|
128
|
+
if (tree.nodes.has(nodeId)) continue;
|
|
129
|
+
|
|
130
|
+
const node: OrgNode = {
|
|
131
|
+
id: nodeId,
|
|
132
|
+
name: raw.name || entry.name,
|
|
133
|
+
level: (raw.level as OrgNode['level']) || 'member',
|
|
134
|
+
reportsTo: (raw.reports_to || 'ceo').toLowerCase(),
|
|
135
|
+
children: [],
|
|
136
|
+
persona: raw.persona || '',
|
|
137
|
+
authority: {
|
|
138
|
+
autonomous: raw.authority?.autonomous ?? [],
|
|
139
|
+
needsApproval: raw.authority?.needs_approval ?? [],
|
|
140
|
+
},
|
|
141
|
+
knowledge: {
|
|
142
|
+
reads: raw.knowledge?.reads ?? [],
|
|
143
|
+
writes: raw.knowledge?.writes ?? [],
|
|
144
|
+
},
|
|
145
|
+
reports: {
|
|
146
|
+
daily: raw.reports?.daily ?? '',
|
|
147
|
+
weekly: raw.reports?.weekly ?? '',
|
|
148
|
+
},
|
|
149
|
+
skills: raw.skills,
|
|
150
|
+
model: raw.model,
|
|
151
|
+
source: raw.source ? {
|
|
152
|
+
id: raw.source.id || '',
|
|
153
|
+
sync: (raw.source.sync as RoleSource['sync']) || 'manual',
|
|
154
|
+
forked_at: raw.source.forked_at,
|
|
155
|
+
upstream_version: raw.source.upstream_version,
|
|
156
|
+
} : undefined,
|
|
157
|
+
heartbeat: raw.heartbeat ? {
|
|
158
|
+
enabled: raw.heartbeat.enabled ?? false,
|
|
159
|
+
intervalSec: raw.heartbeat.intervalSec ?? 120,
|
|
160
|
+
maxTicks: raw.heartbeat.maxTicks ?? 60,
|
|
161
|
+
} : undefined,
|
|
162
|
+
};
|
|
163
|
+
tree.nodes.set(node.id, node);
|
|
164
|
+
} catch {
|
|
165
|
+
// Skip malformed YAML
|
|
166
|
+
}
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
|
|
@@ -231,8 +246,8 @@ export function canConsult(tree: OrgTree, source: string, target: string): boole
|
|
|
231
246
|
}
|
|
232
247
|
|
|
233
248
|
/** Refresh tree (re-read all role.yaml files) */
|
|
234
|
-
export function refreshOrgTree(companyRoot: string): OrgTree {
|
|
235
|
-
return buildOrgTree(companyRoot);
|
|
249
|
+
export function refreshOrgTree(companyRoot: string, presetId?: string): OrgTree {
|
|
250
|
+
return buildOrgTree(companyRoot, presetId);
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
/** Get a human-readable org chart string for context injection */
|
|
@@ -223,6 +223,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
223
223
|
|
|
224
224
|
const targetRoles = body.targetRoles as string[] | undefined;
|
|
225
225
|
const continuous = body.continuous === true;
|
|
226
|
+
const preset = body.preset as string | undefined;
|
|
226
227
|
|
|
227
228
|
// Always use supervisor mode — CEO supervises C-Levels who supervise members
|
|
228
229
|
{
|
|
@@ -231,6 +232,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
231
232
|
actualDirective,
|
|
232
233
|
targetRoles && targetRoles.length > 0 ? targetRoles : undefined,
|
|
233
234
|
continuous,
|
|
235
|
+
preset,
|
|
234
236
|
);
|
|
235
237
|
|
|
236
238
|
if (state.status === 'error') {
|
|
@@ -319,7 +319,7 @@ export function scaffold(config: ScaffoldConfig): string[] {
|
|
|
319
319
|
'operations/decisions', 'operations/activity-streams',
|
|
320
320
|
'operations/sessions', 'operations/cost',
|
|
321
321
|
'knowledge', 'methodologies', '.claude/skills',
|
|
322
|
-
'.claude/skills/_shared', '.tycono',
|
|
322
|
+
'.claude/skills/_shared', '.tycono', 'company/presets',
|
|
323
323
|
];
|
|
324
324
|
for (const dir of dirs) {
|
|
325
325
|
fs.mkdirSync(path.join(root, dir), { recursive: true });
|
|
@@ -28,6 +28,7 @@ interface SupervisorState {
|
|
|
28
28
|
directive: string;
|
|
29
29
|
targetRoles?: string[];
|
|
30
30
|
continuous: boolean;
|
|
31
|
+
preset?: string;
|
|
31
32
|
supervisorSessionId: string | null;
|
|
32
33
|
executionId: string | null;
|
|
33
34
|
status: 'starting' | 'running' | 'restarting' | 'stopped' | 'error';
|
|
@@ -66,7 +67,7 @@ class SupervisorHeartbeat {
|
|
|
66
67
|
* This creates a supervisor session and starts an execution.
|
|
67
68
|
* If the execution dies, it auto-restarts (heartbeat).
|
|
68
69
|
*/
|
|
69
|
-
start(waveId: string, directive: string, targetRoles?: string[], continuous = false): SupervisorState {
|
|
70
|
+
start(waveId: string, directive: string, targetRoles?: string[], continuous = false, preset?: string): SupervisorState {
|
|
70
71
|
// Check if supervisor already running for this wave
|
|
71
72
|
const existing = this.supervisors.get(waveId);
|
|
72
73
|
if (existing && (existing.status === 'running' || existing.status === 'starting')) {
|
|
@@ -79,6 +80,7 @@ class SupervisorHeartbeat {
|
|
|
79
80
|
directive,
|
|
80
81
|
targetRoles,
|
|
81
82
|
continuous,
|
|
83
|
+
preset,
|
|
82
84
|
supervisorSessionId: null,
|
|
83
85
|
executionId: null,
|
|
84
86
|
status: 'starting',
|
|
@@ -100,7 +102,7 @@ class SupervisorHeartbeat {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
// Save wave file immediately so directive persists across restarts
|
|
103
|
-
this.saveWaveFile(waveId, directive);
|
|
105
|
+
this.saveWaveFile(waveId, directive, preset);
|
|
104
106
|
|
|
105
107
|
this.spawnSupervisor(state);
|
|
106
108
|
return state;
|
|
@@ -110,20 +112,22 @@ class SupervisorHeartbeat {
|
|
|
110
112
|
* Save wave file immediately so directive persists across restarts.
|
|
111
113
|
* saveCompletedWave() adds session/role details on completion.
|
|
112
114
|
*/
|
|
113
|
-
private saveWaveFile(waveId: string, directive: string): void {
|
|
115
|
+
private saveWaveFile(waveId: string, directive: string, preset?: string): void {
|
|
114
116
|
try {
|
|
115
117
|
const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
|
|
116
118
|
if (!fs.existsSync(wavesDir)) fs.mkdirSync(wavesDir, { recursive: true });
|
|
117
119
|
const wavePath = path.join(wavesDir, `${waveId}.json`);
|
|
118
120
|
if (!fs.existsSync(wavePath)) {
|
|
119
|
-
|
|
121
|
+
const waveData: Record<string, unknown> = {
|
|
120
122
|
id: waveId,
|
|
121
123
|
waveId,
|
|
122
124
|
directive,
|
|
123
125
|
startedAt: new Date().toISOString(),
|
|
124
126
|
sessionIds: [],
|
|
125
127
|
roles: [],
|
|
126
|
-
}
|
|
128
|
+
};
|
|
129
|
+
if (preset) waveData.preset = preset;
|
|
130
|
+
fs.writeFileSync(wavePath, JSON.stringify(waveData, null, 2));
|
|
127
131
|
console.log(`[Supervisor] Wave file created: ${wavePath}`);
|
|
128
132
|
}
|
|
129
133
|
} catch (err) {
|
|
@@ -510,7 +514,7 @@ Do NOT dispatch anyone. Do NOT create new files. Just answer concisely.`;
|
|
|
510
514
|
/* ─── Internal: Spawn / Restart ────────────── */
|
|
511
515
|
|
|
512
516
|
private spawnSupervisor(state: SupervisorState): void {
|
|
513
|
-
const orgTree = buildOrgTree(COMPANY_ROOT);
|
|
517
|
+
const orgTree = buildOrgTree(COMPANY_ROOT, state.preset);
|
|
514
518
|
let cLevelRoles = getSubordinates(orgTree, 'ceo');
|
|
515
519
|
|
|
516
520
|
if (state.targetRoles && state.targetRoles.length > 0) {
|
|
@@ -304,7 +304,16 @@ export function saveCompletedWave(waveId: string, directive: string): { ok: bool
|
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
// Preserve preset field from existing wave file
|
|
308
|
+
let existingPreset: string | undefined;
|
|
309
|
+
if (existing) {
|
|
310
|
+
try {
|
|
311
|
+
const existingData = JSON.parse(fs.readFileSync(existing, 'utf-8'));
|
|
312
|
+
existingPreset = existingData.preset;
|
|
313
|
+
} catch { /* ignore */ }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const waveJson: Record<string, unknown> = {
|
|
308
317
|
id: baseName,
|
|
309
318
|
directive,
|
|
310
319
|
startedAt: startedAt.toISOString(),
|
|
@@ -313,6 +322,7 @@ export function saveCompletedWave(waveId: string, directive: string): { ok: bool
|
|
|
313
322
|
waveId,
|
|
314
323
|
sessionIds: allSessionIds,
|
|
315
324
|
};
|
|
325
|
+
if (existingPreset) waveJson.preset = existingPreset;
|
|
316
326
|
fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
|
|
317
327
|
|
|
318
328
|
const relativePath = `operations/waves/${baseName}.json`;
|
package/src/tui/app.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import { useApi } from './hooks/useApi';
|
|
|
21
21
|
import { useSSE } from './hooks/useSSE';
|
|
22
22
|
import { useCommand, type WaveInfo } from './hooks/useCommand';
|
|
23
23
|
import { dispatchWave } from './api';
|
|
24
|
-
import type { ActiveSessionInfo } from './api';
|
|
24
|
+
import type { ActiveSessionInfo, PresetSummary } from './api';
|
|
25
25
|
import { buildOrgTree, flattenOrgRoleIds } from './store';
|
|
26
26
|
|
|
27
27
|
type Mode = 'command' | 'panel';
|
|
@@ -231,6 +231,10 @@ export const App: React.FC = () => {
|
|
|
231
231
|
// System messages (command feedback displayed in stream area)
|
|
232
232
|
const [systemMessages, setSystemMessages] = useState<StreamLine[]>([]);
|
|
233
233
|
|
|
234
|
+
// Preset selection state (for /new without args)
|
|
235
|
+
const [pendingPresetSelect, setPendingPresetSelect] = useState<PresetSummary[] | null>(null);
|
|
236
|
+
const selectedPresetRef = useRef<string | null>(null);
|
|
237
|
+
|
|
234
238
|
// Terminal full height with resize tracking (minus 1 for wide-char overflow safety)
|
|
235
239
|
const [termHeight, setTermHeight] = useState((process.stdout.rows || 30) - 1);
|
|
236
240
|
|
|
@@ -337,21 +341,24 @@ export const App: React.FC = () => {
|
|
|
337
341
|
return waves.find(w => w.waveId === focusedWaveId)?.startedAt ?? 0;
|
|
338
342
|
}, [focusedWaveId, waves]);
|
|
339
343
|
|
|
344
|
+
// Wave creation callback — shared by useCommand and preset selection flow
|
|
345
|
+
const onWaveCreated = useCallback((newWaveId: string, directive: string) => {
|
|
346
|
+
const newWave: WaveInfo = {
|
|
347
|
+
waveId: newWaveId,
|
|
348
|
+
directive,
|
|
349
|
+
startedAt: Date.now(),
|
|
350
|
+
};
|
|
351
|
+
setWaves(prev => [...prev, newWave]);
|
|
352
|
+
setFocusedWaveId(newWaveId);
|
|
353
|
+
sse.clearEvents();
|
|
354
|
+
api.refresh();
|
|
355
|
+
}, [sse, api]);
|
|
356
|
+
|
|
340
357
|
// Command handler
|
|
341
358
|
const { execute } = useCommand({
|
|
342
359
|
focusedWaveId,
|
|
343
360
|
waves,
|
|
344
|
-
onWaveCreated
|
|
345
|
-
const newWave: WaveInfo = {
|
|
346
|
-
waveId: newWaveId,
|
|
347
|
-
directive,
|
|
348
|
-
startedAt: Date.now(),
|
|
349
|
-
};
|
|
350
|
-
setWaves(prev => [...prev, newWave]);
|
|
351
|
-
setFocusedWaveId(newWaveId);
|
|
352
|
-
sse.clearEvents();
|
|
353
|
-
api.refresh();
|
|
354
|
-
},
|
|
361
|
+
onWaveCreated,
|
|
355
362
|
onFocusWave: (waveId) => {
|
|
356
363
|
setFocusedWaveId(waveId);
|
|
357
364
|
sse.clearEvents();
|
|
@@ -366,6 +373,44 @@ export const App: React.FC = () => {
|
|
|
366
373
|
const handleCommandSubmit = useCallback(async (input: string) => {
|
|
367
374
|
// User input is already shown by CommandMode (immediate commit to Static)
|
|
368
375
|
|
|
376
|
+
// Preset selection mode: user types a number to pick preset
|
|
377
|
+
if (pendingPresetSelect) {
|
|
378
|
+
const trimmed = input.trim();
|
|
379
|
+
const idx = parseInt(trimmed, 10);
|
|
380
|
+
if (!isNaN(idx) && idx >= 1 && idx <= pendingPresetSelect.length) {
|
|
381
|
+
const selected = pendingPresetSelect[idx - 1];
|
|
382
|
+
setPendingPresetSelect(null);
|
|
383
|
+
addSystemMessage(`Selected: ${selected.name}. Type your directive:`, 'cyan');
|
|
384
|
+
// Store selected preset for next input
|
|
385
|
+
selectedPresetRef.current = selected.id;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// If user typed text instead of number, treat as directive with selected/default preset
|
|
389
|
+
const presetId = selectedPresetRef.current || 'default';
|
|
390
|
+
setPendingPresetSelect(null);
|
|
391
|
+
selectedPresetRef.current = null;
|
|
392
|
+
try {
|
|
393
|
+
const waveResult = await dispatchWave(trimmed || undefined, { preset: presetId });
|
|
394
|
+
onWaveCreated(waveResult.waveId, trimmed);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
addSystemMessage(`Wave failed: ${err instanceof Error ? err.message : 'unknown'}`, 'red');
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// If a preset was selected previously, this input is the directive
|
|
402
|
+
if (selectedPresetRef.current) {
|
|
403
|
+
const presetId = selectedPresetRef.current;
|
|
404
|
+
selectedPresetRef.current = null;
|
|
405
|
+
try {
|
|
406
|
+
const waveResult = await dispatchWave(input.trim() || undefined, { preset: presetId });
|
|
407
|
+
onWaveCreated(waveResult.waveId, input.trim());
|
|
408
|
+
} catch (err) {
|
|
409
|
+
addSystemMessage(`Wave failed: ${err instanceof Error ? err.message : 'unknown'}`, 'red');
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
369
414
|
const result = await execute(input);
|
|
370
415
|
|
|
371
416
|
switch (result.type) {
|
|
@@ -513,6 +558,7 @@ export const App: React.FC = () => {
|
|
|
513
558
|
addSystemMessage(' /sessions Sessions + ports (kill/cleanup)', 'white');
|
|
514
559
|
addSystemMessage(' /kill <id> Kill a session', 'white');
|
|
515
560
|
addSystemMessage(' /cleanup Remove dead sessions', 'white');
|
|
561
|
+
addSystemMessage(' /preset list Installed presets', 'white');
|
|
516
562
|
addSystemMessage(' /help This help', 'white');
|
|
517
563
|
addSystemMessage(' /quit Exit', 'white');
|
|
518
564
|
addSystemMessage('Keys: [Tab] team panel [1-9] wave [Esc] back [Ctrl+C] quit', 'gray');
|
|
@@ -526,6 +572,46 @@ export const App: React.FC = () => {
|
|
|
526
572
|
addSystemMessage(`Sessions: ${api.sessions.length} Active: ${activeCount} Waves: ${waves.length} Ports: ${api.portSummary.totalPorts}`, 'white');
|
|
527
573
|
}
|
|
528
574
|
break;
|
|
575
|
+
case 'preset_list': {
|
|
576
|
+
const presets = result.presets ?? [];
|
|
577
|
+
if (presets.length === 0) {
|
|
578
|
+
addSystemMessage('No presets installed.', 'gray');
|
|
579
|
+
} else {
|
|
580
|
+
addSystemMessage('Installed presets:', 'cyan');
|
|
581
|
+
for (const p of presets) {
|
|
582
|
+
const star = p.isDefault ? ' \u2605' : '';
|
|
583
|
+
const desc = p.description ? ` \u2014 ${p.description}` : '';
|
|
584
|
+
addSystemMessage(` ${p.id} (${p.rolesCount} roles)${desc}${star}`, p.isDefault ? 'green' : 'white');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case 'preset_select': {
|
|
590
|
+
const presets = result.presets ?? [];
|
|
591
|
+
if (presets.length === 0) {
|
|
592
|
+
addSystemMessage('No presets. Creating wave with default team.', 'gray');
|
|
593
|
+
try {
|
|
594
|
+
const waveResult = await dispatchWave();
|
|
595
|
+
onWaveCreated(waveResult.waveId, '');
|
|
596
|
+
} catch { /* ignore */ }
|
|
597
|
+
} else if (presets.length === 1) {
|
|
598
|
+
// Only default → show prompt to enter directive
|
|
599
|
+
addSystemMessage('Only default preset available. Type your directive:', 'gray');
|
|
600
|
+
} else {
|
|
601
|
+
// Multiple presets → show selection
|
|
602
|
+
addSystemMessage('Select a team preset for this wave:', 'cyan');
|
|
603
|
+
for (let i = 0; i < presets.length; i++) {
|
|
604
|
+
const p = presets[i];
|
|
605
|
+
const star = p.isDefault ? ' \u2605' : '';
|
|
606
|
+
const desc = p.description ? ` \u2014 ${p.description}` : '';
|
|
607
|
+
addSystemMessage(` ${i + 1}. ${p.name} (${p.rolesCount} roles)${desc}${star}`, p.isDefault ? 'green' : 'white');
|
|
608
|
+
}
|
|
609
|
+
addSystemMessage('Type a number to select, then enter your directive.', 'gray');
|
|
610
|
+
// Store presets for number selection — handled via pendingPresetSelect
|
|
611
|
+
setPendingPresetSelect(presets);
|
|
612
|
+
}
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
529
615
|
case 'panel':
|
|
530
616
|
break;
|
|
531
617
|
case 'quit':
|
|
@@ -535,7 +621,7 @@ export const App: React.FC = () => {
|
|
|
535
621
|
addSystemMessage(result.message, 'green');
|
|
536
622
|
}
|
|
537
623
|
}
|
|
538
|
-
}, [execute, addSystemMessage, addSystemLines, focusedWaveId, focusedWaveIndex, derivedWaveStatus, api.sessions.length, activeCount, waves, api.activeSessions, api.portSummary]);
|
|
624
|
+
}, [execute, addSystemMessage, addSystemLines, focusedWaveId, focusedWaveIndex, derivedWaveStatus, api.sessions.length, activeCount, waves, api.activeSessions, api.portSummary, pendingPresetSelect, onWaveCreated]);
|
|
539
625
|
|
|
540
626
|
// Global key handler: Tab to toggle mode, Ctrl+C always exits
|
|
541
627
|
useInput((input, key) => {
|
|
@@ -299,6 +299,7 @@ const COMMANDS: Array<{ cmd: string; desc: string }> = [
|
|
|
299
299
|
{ cmd: '/sessions', desc: 'Active sessions' },
|
|
300
300
|
{ cmd: '/kill <id>', desc: 'Kill session' },
|
|
301
301
|
{ cmd: '/cleanup', desc: 'Remove dead' },
|
|
302
|
+
{ cmd: '/preset list', desc: 'Installed presets' },
|
|
302
303
|
{ cmd: '/help', desc: 'Help' },
|
|
303
304
|
{ cmd: '/quit', desc: 'Exit' },
|
|
304
305
|
];
|
|
@@ -139,7 +139,7 @@ function readFilePreview(filePath: string, maxLines: number): string[] {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export const PanelMode: React.FC<PanelModeProps> = ({
|
|
142
|
+
export const PanelMode: React.FC<PanelModeProps> = React.memo(({
|
|
143
143
|
tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
|
|
144
144
|
streamStatus, waveId, activeSessions, allSessions, companyRoot, waves,
|
|
145
145
|
focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
|
|
@@ -545,4 +545,4 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
545
545
|
</Box>
|
|
546
546
|
</Box>
|
|
547
547
|
);
|
|
548
|
-
};
|
|
548
|
+
}));
|
|
@@ -116,7 +116,7 @@ function renderEvent(event: SSEEvent): { content: string; contentColor: string }
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
export const StreamView: React.FC<StreamViewProps> = ({
|
|
119
|
+
export const StreamView: React.FC<StreamViewProps> = React.memo(({
|
|
120
120
|
events,
|
|
121
121
|
allRoleIds,
|
|
122
122
|
streamStatus,
|
|
@@ -162,10 +162,10 @@ export const StreamView: React.FC<StreamViewProps> = ({
|
|
|
162
162
|
<Box key={`${event.seq}-${i}`}>
|
|
163
163
|
<Text color="gray" dimColor>{formatTime(event.ts)} </Text>
|
|
164
164
|
<Text color={roleColor} bold>{event.roleId.padEnd(12)}</Text>
|
|
165
|
-
<Text color={rendered.contentColor} wrap="
|
|
165
|
+
<Text color={rendered.contentColor} wrap="truncate">{rendered.content}</Text>
|
|
166
166
|
</Box>
|
|
167
167
|
);
|
|
168
168
|
})}
|
|
169
169
|
</Box>
|
|
170
170
|
);
|
|
171
|
-
};
|
|
171
|
+
}));
|