tycono 0.3.14-beta.1 → 0.3.14-beta.10
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/create-server.ts +2 -0
- package/src/api/src/routes/presets.ts +92 -4
- package/src/api/src/services/execution-manager.ts +4 -3
- package/src/api/src/services/supervisor-heartbeat.ts +4 -1
- package/src/tui/app.tsx +46 -1
- package/src/tui/components/OrgTree.tsx +15 -82
- package/src/tui/components/PanelMode.tsx +1 -1
- package/src/tui/components/StreamView.tsx +45 -113
- package/src/web/dist/assets/index-C6r_vHBI.js +138 -0
- package/src/web/dist/assets/{index-uwS0YSTU.js → index-Czp8wshq.js} +1 -1
- package/src/web/dist/assets/index-DVKWFwwK.css +1 -0
- package/src/web/dist/assets/{preview-app-CAohaHWp.js → preview-app-CMGFfqT-.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-A3-TBmWZ.js +0 -138
- package/src/web/dist/assets/index-D1RTvnx7.css +0 -1
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@ import { questsRouter } from './routes/quests.js';
|
|
|
33
33
|
import { coinsRouter } from './routes/coins.js';
|
|
34
34
|
import { activeSessionsRouter } from './routes/active-sessions.js';
|
|
35
35
|
import { supervisionRouter } from './routes/supervision.js';
|
|
36
|
+
import { presetsRouter } from './routes/presets.js';
|
|
36
37
|
import { importKnowledge } from './services/knowledge-importer.js';
|
|
37
38
|
import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
|
|
38
39
|
import { readConfig } from './services/company-config.js';
|
|
@@ -212,6 +213,7 @@ export function createExpressApp(): express.Application {
|
|
|
212
213
|
app.use('/api/coins', coinsRouter);
|
|
213
214
|
app.use('/api/active-sessions', activeSessionsRouter);
|
|
214
215
|
app.use('/api/supervision', supervisionRouter);
|
|
216
|
+
app.use('/api/presets', presetsRouter);
|
|
215
217
|
|
|
216
218
|
app.get('/api/health', (_req, res) => {
|
|
217
219
|
res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* presets.ts — Preset API routes
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
5
|
-
* GET
|
|
4
|
+
* GET /api/presets — list all preset summaries
|
|
5
|
+
* GET /api/presets/:id — get full preset detail
|
|
6
|
+
* POST /api/presets/install — install preset from data
|
|
7
|
+
* DELETE /api/presets/:id — remove installed preset
|
|
6
8
|
*/
|
|
7
9
|
import { Router } from 'express';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import YAML from 'yaml';
|
|
8
13
|
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
9
|
-
import { getPresetSummaries, getPresetById } from '../services/preset-loader.js';
|
|
14
|
+
import { getPresetSummaries, getPresetById, loadPresets } from '../services/preset-loader.js';
|
|
10
15
|
|
|
11
16
|
export const presetsRouter = Router();
|
|
12
17
|
|
|
13
|
-
/** GET /api/presets — list preset summaries
|
|
18
|
+
/** GET /api/presets — list preset summaries */
|
|
14
19
|
presetsRouter.get('/', (_req, res) => {
|
|
15
20
|
try {
|
|
16
21
|
const summaries = getPresetSummaries(COMPANY_ROOT);
|
|
@@ -33,3 +38,86 @@ presetsRouter.get('/:id', (req, res) => {
|
|
|
33
38
|
res.status(500).json({ error: 'Failed to load preset' });
|
|
34
39
|
}
|
|
35
40
|
});
|
|
41
|
+
|
|
42
|
+
/** POST /api/presets/install — install a preset from provided data */
|
|
43
|
+
presetsRouter.post('/install', (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const { id, preset } = req.body as { id: string; preset: Record<string, unknown> };
|
|
46
|
+
if (!id || !preset) {
|
|
47
|
+
res.status(400).json({ error: 'id and preset are required' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate preset has required fields
|
|
52
|
+
if (!preset.name || !preset.roles || !Array.isArray(preset.roles)) {
|
|
53
|
+
res.status(400).json({ error: 'preset must have name and roles array' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for conflict with existing preset
|
|
58
|
+
const existing = getPresetById(COMPANY_ROOT, id);
|
|
59
|
+
if (existing && !existing.isDefault) {
|
|
60
|
+
res.status(409).json({ error: `Preset already installed: ${id}` });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create preset directory and write preset.yaml
|
|
65
|
+
const presetDir = path.join(COMPANY_ROOT, 'company', 'presets', id);
|
|
66
|
+
fs.mkdirSync(presetDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
// Write preset.yaml
|
|
69
|
+
const yamlContent = YAML.stringify(preset);
|
|
70
|
+
fs.writeFileSync(path.join(presetDir, 'preset.yaml'), yamlContent);
|
|
71
|
+
|
|
72
|
+
// Create subdirectories for roles/knowledge/skills
|
|
73
|
+
fs.mkdirSync(path.join(presetDir, 'roles'), { recursive: true });
|
|
74
|
+
fs.mkdirSync(path.join(presetDir, 'knowledge'), { recursive: true });
|
|
75
|
+
fs.mkdirSync(path.join(presetDir, 'skills'), { recursive: true });
|
|
76
|
+
|
|
77
|
+
// Write knowledge docs if provided
|
|
78
|
+
const knowledge = req.body.knowledge as Array<{ filename: string; content: string }> | undefined;
|
|
79
|
+
if (knowledge) {
|
|
80
|
+
for (const doc of knowledge) {
|
|
81
|
+
fs.writeFileSync(path.join(presetDir, 'knowledge', doc.filename), doc.content);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Write role yamls if provided
|
|
86
|
+
const roleDefinitions = req.body.roleDefinitions as Array<{ id: string; yaml: string }> | undefined;
|
|
87
|
+
if (roleDefinitions) {
|
|
88
|
+
for (const role of roleDefinitions) {
|
|
89
|
+
const roleDir = path.join(presetDir, 'roles', role.id);
|
|
90
|
+
fs.mkdirSync(roleDir, { recursive: true });
|
|
91
|
+
fs.writeFileSync(path.join(roleDir, 'role.yaml'), role.yaml);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
res.json({ ok: true, id, path: `company/presets/${id}` });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
res.status(500).json({ error: `Install failed: ${err instanceof Error ? err.message : 'unknown'}` });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** DELETE /api/presets/:id — remove installed preset */
|
|
102
|
+
presetsRouter.delete('/:id', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const { id } = req.params;
|
|
105
|
+
if (id === 'default' || id === '_default') {
|
|
106
|
+
res.status(400).json({ error: 'Cannot remove default preset' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const presetDir = path.join(COMPANY_ROOT, 'company', 'presets', id);
|
|
111
|
+
if (!fs.existsSync(presetDir)) {
|
|
112
|
+
res.status(404).json({ error: `Preset not found: ${id}` });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remove preset directory recursively
|
|
117
|
+
fs.rmSync(presetDir, { recursive: true, force: true });
|
|
118
|
+
|
|
119
|
+
res.json({ ok: true, id });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
res.status(500).json({ error: `Remove failed: ${err instanceof Error ? err.message : 'unknown'}` });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -180,7 +180,7 @@ class ExecutionManager {
|
|
|
180
180
|
|
|
181
181
|
this.executions.set(execId, execution);
|
|
182
182
|
|
|
183
|
-
this.initializeAndRunExecution(execution, params, orgTree);
|
|
183
|
+
this.initializeAndRunExecution(execution, params, orgTree, presetId);
|
|
184
184
|
|
|
185
185
|
return execution;
|
|
186
186
|
}
|
|
@@ -189,6 +189,7 @@ class ExecutionManager {
|
|
|
189
189
|
execution: Execution,
|
|
190
190
|
params: StartExecutionParams,
|
|
191
191
|
orgTree: ReturnType<typeof buildOrgTree>,
|
|
192
|
+
presetId?: string,
|
|
192
193
|
): Promise<void> {
|
|
193
194
|
try {
|
|
194
195
|
const ports = await portRegistry.allocate(execution.sessionId || execution.id, params.roleId, params.task);
|
|
@@ -724,8 +725,8 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
|
|
|
724
725
|
console.log(`[ExecMgr] Supervision recovery: ${deadExecution.roleId} died with ${runningChildren.length} running children. Restarting.`);
|
|
725
726
|
|
|
726
727
|
// Propagate waveId from the dead session
|
|
727
|
-
const
|
|
728
|
-
const waveId =
|
|
728
|
+
const deadSes = getSession(deadExecution.sessionId);
|
|
729
|
+
const waveId = deadSes?.waveId;
|
|
729
730
|
|
|
730
731
|
// Create new session for recovery
|
|
731
732
|
const newSession = createSession(deadExecution.roleId, {
|
|
@@ -172,13 +172,15 @@ class SupervisorHeartbeat {
|
|
|
172
172
|
const waveSessions = listSessions().filter(s => s.waveId === waveId);
|
|
173
173
|
const ceoSession = waveSessions.find(s => s.roleId === 'ceo') ?? null;
|
|
174
174
|
|
|
175
|
-
// Read original directive from wave artifact file
|
|
175
|
+
// Read original directive + preset from wave artifact file
|
|
176
176
|
let originalDirective = '';
|
|
177
|
+
let originalPreset: string | undefined;
|
|
177
178
|
try {
|
|
178
179
|
const waveFile = path.join(COMPANY_ROOT, 'operations', 'waves', `${waveId}.json`);
|
|
179
180
|
if (fs.existsSync(waveFile)) {
|
|
180
181
|
const waveData = JSON.parse(fs.readFileSync(waveFile, 'utf-8'));
|
|
181
182
|
originalDirective = waveData.directive ?? '';
|
|
183
|
+
originalPreset = waveData.preset;
|
|
182
184
|
}
|
|
183
185
|
} catch { /* ignore */ }
|
|
184
186
|
|
|
@@ -188,6 +190,7 @@ class SupervisorHeartbeat {
|
|
|
188
190
|
waveId,
|
|
189
191
|
directive: originalDirective || text,
|
|
190
192
|
continuous: false,
|
|
193
|
+
preset: originalPreset,
|
|
191
194
|
supervisorSessionId: ceoSession?.id ?? null,
|
|
192
195
|
executionId: null,
|
|
193
196
|
status: 'stopped',
|
package/src/tui/app.tsx
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
15
15
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
16
16
|
import { StatusBar } from './components/StatusBar';
|
|
17
|
+
import { OrgTree } from './components/OrgTree';
|
|
18
|
+
import { StreamView } from './components/StreamView';
|
|
17
19
|
import { CommandMode, type StreamLine } from './components/CommandMode';
|
|
18
20
|
import { PanelMode } from './components/PanelMode';
|
|
19
21
|
import { SetupWizard } from './components/SetupWizard';
|
|
@@ -668,8 +670,51 @@ export const App: React.FC = () => {
|
|
|
668
670
|
// Command Mode: scrollable terminal (no fullscreen)
|
|
669
671
|
// Panel Mode: fullscreen (intentional — like vim for inspection)
|
|
670
672
|
if (mode === 'panel') {
|
|
673
|
+
// OOM debug levels: 0=full, 1=minimal, 2=orgTree only, 3=stream only
|
|
674
|
+
const debugLevel = parseInt(process.env.PANEL_MINIMAL || '0', 10);
|
|
675
|
+
if (debugLevel === 1) {
|
|
676
|
+
return (
|
|
677
|
+
<Box flexDirection="column">
|
|
678
|
+
<Text color="cyan">Panel Mode (minimal)</Text>
|
|
679
|
+
<Text color="gray">Events: {sse.events.length} | Press Esc</Text>
|
|
680
|
+
</Box>
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
if (debugLevel === 2) {
|
|
684
|
+
return (
|
|
685
|
+
<Box flexDirection="column">
|
|
686
|
+
<OrgTree tree={orgTree} focused={true} selectedIndex={0} flatRoles={flatRoleIds} ceoStatus="idle" />
|
|
687
|
+
<Text color="gray">OrgTree only | Press Esc</Text>
|
|
688
|
+
</Box>
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
if (debugLevel === 3) {
|
|
692
|
+
return (
|
|
693
|
+
<Box flexDirection="column">
|
|
694
|
+
<StreamView events={sse.events} allRoleIds={flatRoleIds} streamStatus={sse.streamStatus} waveId={focusedWaveId} roleLabel="All" />
|
|
695
|
+
<Text color="gray">StreamView only | Press Esc</Text>
|
|
696
|
+
</Box>
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (debugLevel === 4) {
|
|
700
|
+
// Full layout structure but empty content
|
|
701
|
+
return (
|
|
702
|
+
<Box flexDirection="column" height={termHeight}>
|
|
703
|
+
<Box flexGrow={1}>
|
|
704
|
+
<Box flexDirection="column" width={28}>
|
|
705
|
+
<Text color="green">Left Panel</Text>
|
|
706
|
+
</Box>
|
|
707
|
+
<Text color="gray">{'\u2502'}</Text>
|
|
708
|
+
<Box flexGrow={1} flexDirection="column" overflow="hidden">
|
|
709
|
+
<Text color="cyan">Right Panel</Text>
|
|
710
|
+
</Box>
|
|
711
|
+
</Box>
|
|
712
|
+
<StatusBar companyName="test" waveIndex={1} waveCount={1} waveStatus="idle" activeCount={0} portCount={0} totalCost={0} />
|
|
713
|
+
</Box>
|
|
714
|
+
);
|
|
715
|
+
}
|
|
671
716
|
return (
|
|
672
|
-
<Box flexDirection="column"
|
|
717
|
+
<Box flexDirection="column">
|
|
673
718
|
<Box flexGrow={1} flexDirection="column">
|
|
674
719
|
<PanelMode
|
|
675
720
|
tree={orgTree}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OrgTree — left panel showing organization hierarchy
|
|
3
|
-
*
|
|
2
|
+
* OrgTree — left panel showing organization hierarchy
|
|
3
|
+
* Simplified to single Text render to prevent yoga OOM on wide terminals
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from 'react';
|
|
@@ -16,103 +16,36 @@ interface OrgTreeProps {
|
|
|
16
16
|
ceoStatus?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
case 'working':
|
|
22
|
-
case 'streaming':
|
|
23
|
-
return 'green';
|
|
24
|
-
case 'done':
|
|
25
|
-
return 'gray';
|
|
26
|
-
case 'error':
|
|
27
|
-
return 'red';
|
|
28
|
-
case 'awaiting_input':
|
|
29
|
-
return 'yellow';
|
|
30
|
-
default:
|
|
31
|
-
return 'gray';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface FlatEntry {
|
|
36
|
-
roleId: string;
|
|
37
|
-
level: string;
|
|
38
|
-
status: string;
|
|
39
|
-
prefix: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function flattenTree(nodes: OrgNode[], prefix: string = '', isLast: boolean[] = []): FlatEntry[] {
|
|
43
|
-
const result: FlatEntry[] = [];
|
|
44
|
-
|
|
19
|
+
function flattenTree(nodes: OrgNode[], isLast: boolean[] = []): Array<{ roleId: string; status: string; line: string }> {
|
|
20
|
+
const result: Array<{ roleId: string; status: string; line: string }> = [];
|
|
45
21
|
for (let i = 0; i < nodes.length; i++) {
|
|
46
22
|
const node = nodes[i];
|
|
47
23
|
const last = i === nodes.length - 1;
|
|
48
|
-
|
|
49
|
-
let linePrefix = '';
|
|
24
|
+
let prefix = '';
|
|
50
25
|
for (let j = 0; j < isLast.length; j++) {
|
|
51
|
-
|
|
26
|
+
prefix += isLast[j] ? ' ' : '\u2502 ';
|
|
52
27
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
result.push({
|
|
58
|
-
roleId: node.role.id,
|
|
59
|
-
level: node.role.level,
|
|
60
|
-
status: node.status,
|
|
61
|
-
prefix: linePrefix,
|
|
62
|
-
});
|
|
63
|
-
|
|
28
|
+
prefix += last ? '\u2514\u2500 ' : '\u251C\u2500 ';
|
|
29
|
+
const icon = statusIcon(node.status);
|
|
30
|
+
result.push({ roleId: node.role.id, status: node.status, line: `${prefix}${icon} ${node.role.id}` });
|
|
64
31
|
if (node.children.length > 0) {
|
|
65
|
-
result.push(...flattenTree(node.children,
|
|
32
|
+
result.push(...flattenTree(node.children, [...isLast, last]));
|
|
66
33
|
}
|
|
67
34
|
}
|
|
68
|
-
|
|
69
35
|
return result;
|
|
70
36
|
}
|
|
71
37
|
|
|
72
38
|
export const OrgTree: React.FC<OrgTreeProps> = React.memo(({ tree, focused, selectedIndex, flatRoles, ceoStatus }) => {
|
|
73
|
-
const entries = flattenTree(tree);
|
|
74
|
-
const isCeoSelected = focused && flatRoles[selectedIndex] === 'ceo';
|
|
75
39
|
const ceoIcon = statusIcon(ceoStatus ?? 'idle');
|
|
76
|
-
const
|
|
40
|
+
const entries = flattenTree(tree);
|
|
41
|
+
|
|
42
|
+
// Render entire tree as single Text block (1 yoga node instead of 50+)
|
|
43
|
+
const lines = [`${ceoIcon} CEO`, ...entries.map(e => e.line)];
|
|
77
44
|
|
|
78
45
|
return (
|
|
79
46
|
<Box flexDirection="column" paddingX={1}>
|
|
80
47
|
<Text bold color={focused ? 'cyan' : 'gray'}>{'\u2500\u2500 Org Tree \u2500\u2500'}</Text>
|
|
81
|
-
<
|
|
82
|
-
<Text color={ceoColor} bold={ceoStatus === 'working'}>{ceoIcon} </Text>
|
|
83
|
-
<Text
|
|
84
|
-
color={isCeoSelected ? 'cyan' : 'yellow'}
|
|
85
|
-
bold={isCeoSelected}
|
|
86
|
-
inverse={isCeoSelected}
|
|
87
|
-
>
|
|
88
|
-
CEO
|
|
89
|
-
</Text>
|
|
90
|
-
</Box>
|
|
91
|
-
{entries.map((entry, i) => {
|
|
92
|
-
const isSelected = focused && flatRoles[selectedIndex] === entry.roleId;
|
|
93
|
-
const icon = statusIcon(entry.status);
|
|
94
|
-
const color = statusColor(entry.status);
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<Box key={entry.roleId + '-' + i}>
|
|
98
|
-
<Text color="gray">{entry.prefix}</Text>
|
|
99
|
-
<Text
|
|
100
|
-
color={color}
|
|
101
|
-
bold={entry.status === 'working'}
|
|
102
|
-
>
|
|
103
|
-
{icon}
|
|
104
|
-
</Text>
|
|
105
|
-
<Text> </Text>
|
|
106
|
-
<Text
|
|
107
|
-
color={isSelected ? 'cyan' : 'white'}
|
|
108
|
-
bold={isSelected}
|
|
109
|
-
inverse={isSelected}
|
|
110
|
-
>
|
|
111
|
-
{entry.roleId}
|
|
112
|
-
</Text>
|
|
113
|
-
</Box>
|
|
114
|
-
);
|
|
115
|
-
})}
|
|
48
|
+
<Text color="white">{'\n' + lines.join('\n')}</Text>
|
|
116
49
|
</Box>
|
|
117
50
|
);
|
|
118
51
|
});
|
|
@@ -401,7 +401,7 @@ const PanelModeInner: React.FC<PanelModeProps> = ({
|
|
|
401
401
|
<Text color="gray">{separatorStr}</Text>
|
|
402
402
|
|
|
403
403
|
{/* Right: Tabbed panel */}
|
|
404
|
-
<Box flexGrow={1} flexDirection="column"
|
|
404
|
+
<Box flexGrow={1} flexDirection="column">
|
|
405
405
|
{/* Tab bar */}
|
|
406
406
|
<Box paddingX={1} marginBottom={0}>
|
|
407
407
|
{(['stream', 'docs', 'info'] as RightTab[]).map(tab => (
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* StreamView —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* StreamView — stream panel for Panel Mode
|
|
3
|
+
* Simplified to single Text render to prevent yoga OOM on wide terminals.
|
|
4
|
+
* Previous: 30 events × 3 React elements = 90 yoga nodes → OOM on 245+ columns
|
|
5
|
+
* Now: 1 Text element with pre-formatted string
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import React from 'react';
|
|
8
9
|
import { Box, Text } from 'ink';
|
|
9
10
|
import type { SSEEvent } from '../api';
|
|
10
|
-
import { getRoleColor } from '../theme';
|
|
11
11
|
|
|
12
12
|
interface StreamViewProps {
|
|
13
13
|
events: SSEEvent[];
|
|
@@ -21,111 +21,58 @@ function formatTime(ts: string): string {
|
|
|
21
21
|
try {
|
|
22
22
|
const d = new Date(ts);
|
|
23
23
|
return d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
24
|
-
} catch {
|
|
25
|
-
return '--:--:--';
|
|
26
|
-
}
|
|
24
|
+
} catch { return '--:--:--'; }
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
content: `\u25B6 Started: ${(event.data.task as string)?.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 80) ?? ''}`,
|
|
34
|
-
contentColor: 'green',
|
|
35
|
-
};
|
|
27
|
+
function eventToLine(event: SSEEvent): string | null {
|
|
28
|
+
const time = formatTime(event.ts);
|
|
29
|
+
const role = event.roleId.padEnd(12);
|
|
36
30
|
|
|
31
|
+
switch (event.type) {
|
|
32
|
+
case 'msg:start': {
|
|
33
|
+
const task = ((event.data.task as string) ?? '').replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 80);
|
|
34
|
+
return `${time} ${role} \u25B6 Started: ${task}`;
|
|
35
|
+
}
|
|
37
36
|
case 'msg:done': {
|
|
38
37
|
const turns = event.data.turns as number | undefined;
|
|
39
|
-
return {
|
|
40
|
-
content: `\u2713 Done${turns ? ` (${turns} turns)` : ''}`,
|
|
41
|
-
contentColor: 'green',
|
|
42
|
-
};
|
|
38
|
+
return `${time} ${role} \u2713 Done${turns ? ` (${turns} turns)` : ''}`;
|
|
43
39
|
}
|
|
44
|
-
|
|
45
40
|
case 'msg:error':
|
|
46
|
-
return {
|
|
47
|
-
content: `\u2717 Error: ${(event.data.error as string ?? event.data.message as string ?? '').slice(0, 120)}`,
|
|
48
|
-
contentColor: 'red',
|
|
49
|
-
};
|
|
50
|
-
|
|
41
|
+
return `${time} ${role} \u2717 ${((event.data.error ?? event.data.message) as string ?? '').slice(0, 80)}`;
|
|
51
42
|
case 'text': {
|
|
52
|
-
const text = ((event.data.text as string) ?? '');
|
|
53
|
-
if (!text
|
|
54
|
-
|
|
55
|
-
return { content: text, contentColor: 'white' };
|
|
43
|
+
const text = ((event.data.text as string) ?? '').trim();
|
|
44
|
+
if (!text) return null;
|
|
45
|
+
return `${time} ${role} ${text.slice(0, 120)}`;
|
|
56
46
|
}
|
|
57
|
-
|
|
58
47
|
case 'thinking': {
|
|
59
|
-
const text = ((event.data.text as string) ?? '').slice(0,
|
|
60
|
-
if (!text
|
|
61
|
-
return {
|
|
48
|
+
const text = ((event.data.text as string) ?? '').trim().slice(0, 100);
|
|
49
|
+
if (!text) return null;
|
|
50
|
+
return `${time} ${role} \uD83D\uDCAD ${text}`;
|
|
62
51
|
}
|
|
63
|
-
|
|
64
52
|
case 'tool:start': {
|
|
65
53
|
const name = (event.data.name as string) ?? 'tool';
|
|
66
|
-
const input = event.data.input;
|
|
54
|
+
const input = event.data.input as Record<string, unknown> | undefined;
|
|
67
55
|
let detail = '';
|
|
68
|
-
if (input
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
else if (
|
|
72
|
-
else if (inp.pattern) detail = ` ${String(inp.pattern)}`;
|
|
73
|
-
else detail = ` ${JSON.stringify(input).slice(0, 80)}`;
|
|
56
|
+
if (input) {
|
|
57
|
+
if (input.file_path) detail = ` ${String(input.file_path).slice(0, 60)}`;
|
|
58
|
+
else if (input.command) detail = ` ${String(input.command).slice(0, 60)}`;
|
|
59
|
+
else if (input.pattern) detail = ` ${String(input.pattern)}`;
|
|
74
60
|
}
|
|
75
|
-
return {
|
|
76
|
-
content: `\u2192 ${name}${detail}`,
|
|
77
|
-
contentColor: 'gray',
|
|
78
|
-
};
|
|
61
|
+
return `${time} ${role} \u2192 ${name}${detail}`;
|
|
79
62
|
}
|
|
80
|
-
|
|
81
|
-
case 'tool:result':
|
|
82
|
-
return {
|
|
83
|
-
content: `\u2190 ${(event.data.name as string) ?? 'tool'} done`,
|
|
84
|
-
contentColor: 'gray',
|
|
85
|
-
};
|
|
86
|
-
|
|
87
63
|
case 'dispatch:start':
|
|
88
|
-
return {
|
|
89
|
-
content: `\u21D2 dispatch ${event.data.targetRole as string ?? ''}: ${(event.data.task as string)?.replace(/\u26D4[^\u26D4]*\u26D4[^"]*/g, '').trim().slice(0, 80) ?? ''}`,
|
|
90
|
-
contentColor: 'yellow',
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
case 'dispatch:done':
|
|
94
|
-
return {
|
|
95
|
-
content: `\u21D0 ${event.data.targetRole as string ?? ''} completed`,
|
|
96
|
-
contentColor: 'yellow',
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
case 'msg:awaiting_input': {
|
|
100
|
-
const question = (event.data.question as string) ?? '';
|
|
101
|
-
return {
|
|
102
|
-
content: question ? `? ${question.slice(0, 120)}` : '? Awaiting input...',
|
|
103
|
-
contentColor: 'yellow',
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Hidden (truly internal only)
|
|
108
|
-
case 'heartbeat:tick':
|
|
109
|
-
case 'heartbeat:skip':
|
|
110
|
-
case 'prompt:assembled':
|
|
111
|
-
case 'trace:response':
|
|
112
|
-
return null;
|
|
113
|
-
|
|
64
|
+
return `${time} ${role} \u21D2 dispatch ${event.data.targetRole as string ?? ''}`;
|
|
114
65
|
default:
|
|
115
66
|
return null;
|
|
116
67
|
}
|
|
117
68
|
}
|
|
118
69
|
|
|
119
70
|
const StreamViewInner: React.FC<StreamViewProps> = ({
|
|
120
|
-
events,
|
|
121
|
-
allRoleIds,
|
|
122
|
-
streamStatus,
|
|
123
|
-
waveId,
|
|
124
|
-
roleLabel,
|
|
71
|
+
events, allRoleIds, streamStatus, waveId, roleLabel,
|
|
125
72
|
}) => {
|
|
126
|
-
const
|
|
73
|
+
const termRows = process.stdout.rows || 40;
|
|
74
|
+
const maxVisible = Math.min(Math.max(5, termRows - 15), 20);
|
|
127
75
|
const visibleEvents = events.slice(-maxVisible);
|
|
128
|
-
|
|
129
76
|
const turnCount = events.filter(e => e.type === 'text' || e.type === 'tool:start').length;
|
|
130
77
|
|
|
131
78
|
const statusLabel = streamStatus === 'streaming' ? '\u25CF streaming'
|
|
@@ -133,39 +80,24 @@ const StreamViewInner: React.FC<StreamViewProps> = ({
|
|
|
133
80
|
: streamStatus === 'error' ? '\u2717 error'
|
|
134
81
|
: 'idle';
|
|
135
82
|
|
|
83
|
+
// Build single text block (1 yoga node instead of 90+)
|
|
84
|
+
const lines = visibleEvents
|
|
85
|
+
.map(e => eventToLine(e))
|
|
86
|
+
.filter(Boolean) as string[];
|
|
87
|
+
|
|
88
|
+
const content = lines.length > 0
|
|
89
|
+
? lines.join('\n')
|
|
90
|
+
: (waveId ? `Streaming... waiting for ${roleLabel !== 'All' ? roleLabel + ' ' : ''}events` : 'No active stream. Dispatch a wave to start.');
|
|
91
|
+
|
|
136
92
|
return (
|
|
137
|
-
<Box flexDirection="column" paddingX={1}
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
Stream ({roleLabel})
|
|
141
|
-
</Text>
|
|
93
|
+
<Box flexDirection="column" paddingX={1}>
|
|
94
|
+
<Text bold color="cyan">
|
|
95
|
+
Stream ({roleLabel}){' '}
|
|
142
96
|
<Text color={streamStatus === 'streaming' ? 'green' : 'gray'}>
|
|
143
97
|
{statusLabel} {turnCount > 0 ? `turn ${turnCount}` : ''}
|
|
144
98
|
</Text>
|
|
145
|
-
</
|
|
146
|
-
|
|
147
|
-
{visibleEvents.length === 0 && (
|
|
148
|
-
<Box marginTop={1}>
|
|
149
|
-
<Text color="gray" dimColor>
|
|
150
|
-
{waveId
|
|
151
|
-
? `Streaming... waiting for ${roleLabel !== 'All' ? roleLabel + ' ' : ''}events`
|
|
152
|
-
: 'No active stream. Dispatch a wave to start.'}
|
|
153
|
-
</Text>
|
|
154
|
-
</Box>
|
|
155
|
-
)}
|
|
156
|
-
|
|
157
|
-
{visibleEvents.map((event, i) => {
|
|
158
|
-
const rendered = renderEvent(event);
|
|
159
|
-
if (!rendered) return null;
|
|
160
|
-
const roleColor = getRoleColor(event.roleId, allRoleIds);
|
|
161
|
-
return (
|
|
162
|
-
<Box key={`${event.seq}-${i}`}>
|
|
163
|
-
<Text color="gray" dimColor>{formatTime(event.ts)} </Text>
|
|
164
|
-
<Text color={roleColor} bold>{event.roleId.padEnd(12)}</Text>
|
|
165
|
-
<Text color={rendered.contentColor} wrap="truncate">{rendered.content}</Text>
|
|
166
|
-
</Box>
|
|
167
|
-
);
|
|
168
|
-
})}
|
|
99
|
+
</Text>
|
|
100
|
+
<Text color="white" wrap="truncate">{content}</Text>
|
|
169
101
|
</Box>
|
|
170
102
|
);
|
|
171
103
|
};
|