tycono 0.3.14-beta.0 → 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/engine/agent-loop.ts +2 -1
- package/src/api/src/engine/context-assembler.ts +30 -1
- package/src/api/src/engine/runners/claude-cli.ts +2 -2
- package/src/api/src/engine/runners/types.ts +2 -0
- package/src/api/src/routes/presets.ts +92 -4
- package/src/api/src/services/execution-manager.ts +33 -5
- 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 +22 -3
- package/src/tui/components/StreamView.tsx +49 -115
- 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 });
|
|
@@ -29,6 +29,7 @@ export interface AgentConfig {
|
|
|
29
29
|
tokenLedger?: TokenLedger; // Token usage ledger (optional)
|
|
30
30
|
attachments?: ImageAttachment[]; // Image attachments for vision
|
|
31
31
|
targetRoles?: string[]; // Selective dispatch scope
|
|
32
|
+
presetId?: string; // Wave-scoped preset for knowledge injection
|
|
32
33
|
// Callbacks
|
|
33
34
|
onText?: (text: string) => void;
|
|
34
35
|
onToolExec?: (name: string, input: Record<string, unknown>) => void;
|
|
@@ -162,7 +163,7 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
|
162
163
|
const llm = config.llm ?? new AnthropicProvider();
|
|
163
164
|
|
|
164
165
|
// 1. Assemble context
|
|
165
|
-
const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus: config.teamStatus, targetRoles: config.targetRoles });
|
|
166
|
+
const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus: config.teamStatus, targetRoles: config.targetRoles, presetId: config.presetId });
|
|
166
167
|
|
|
167
168
|
// Trace: capture assembled prompt for debugging
|
|
168
169
|
config.onPromptAssembled?.(context.systemPrompt, task);
|
|
@@ -51,7 +51,7 @@ export function assembleContext(
|
|
|
51
51
|
task: string,
|
|
52
52
|
sourceRole: string,
|
|
53
53
|
orgTree: OrgTree,
|
|
54
|
-
options?: { teamStatus?: TeamStatus; targetRoles?: string[] },
|
|
54
|
+
options?: { teamStatus?: TeamStatus; targetRoles?: string[]; presetId?: string },
|
|
55
55
|
): AssembledContext {
|
|
56
56
|
const node = orgTree.nodes.get(roleId);
|
|
57
57
|
if (!node) {
|
|
@@ -123,6 +123,14 @@ Use the code repository path for all source code work (reading, writing, buildin
|
|
|
123
123
|
sections.push(preKSection);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// 11. Preset Knowledge (wave-scoped preset docs)
|
|
127
|
+
if (options?.presetId && options.presetId !== 'default') {
|
|
128
|
+
const presetKnowledge = loadPresetKnowledge(companyRoot, options.presetId);
|
|
129
|
+
if (presetKnowledge) {
|
|
130
|
+
sections.push('# Preset Knowledge\n\n' + presetKnowledge);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
// Task는 별도 필드로 분리
|
|
127
135
|
let subordinates = getSubordinates(orgTree, roleId);
|
|
128
136
|
|
|
@@ -262,6 +270,27 @@ ${docList}
|
|
|
262
270
|
> **Knowledging Rule**: Check these documents first. If your work produces new knowledge, update existing docs or create new ones with cross-links.`;
|
|
263
271
|
}
|
|
264
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Load knowledge docs from a preset's knowledge/ directory.
|
|
275
|
+
* Returns concatenated content (capped at 2000 chars per doc).
|
|
276
|
+
*/
|
|
277
|
+
function loadPresetKnowledge(companyRoot: string, presetId: string): string | null {
|
|
278
|
+
const knowledgeDir = path.join(companyRoot, 'company', 'presets', presetId, 'knowledge');
|
|
279
|
+
if (!fs.existsSync(knowledgeDir)) return null;
|
|
280
|
+
|
|
281
|
+
const parts: string[] = [];
|
|
282
|
+
try {
|
|
283
|
+
const entries = fs.readdirSync(knowledgeDir).filter(f => f.endsWith('.md'));
|
|
284
|
+
for (const file of entries.slice(0, 10)) { // Cap at 10 docs
|
|
285
|
+
const content = fs.readFileSync(path.join(knowledgeDir, file), 'utf-8');
|
|
286
|
+
const preview = content.slice(0, 2000);
|
|
287
|
+
parts.push(`## ${file}\n\n${preview}${content.length > 2000 ? '\n\n... (truncated)' : ''}`);
|
|
288
|
+
}
|
|
289
|
+
} catch { /* ignore */ }
|
|
290
|
+
|
|
291
|
+
return parts.length > 0 ? parts.join('\n\n---\n\n') : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
265
294
|
function loadCompanyRules(companyRoot: string): string | null {
|
|
266
295
|
const parts: string[] = [];
|
|
267
296
|
|
|
@@ -407,7 +407,7 @@ else:
|
|
|
407
407
|
*/
|
|
408
408
|
export class ClaudeCliRunner implements ExecutionRunner {
|
|
409
409
|
execute(config: RunnerConfig, callbacks: RunnerCallbacks): RunnerHandle {
|
|
410
|
-
const { companyRoot, roleId, task, sourceRole, orgTree, readOnly = false, teamStatus, attachments, targetRoles } = config;
|
|
410
|
+
const { companyRoot, roleId, task, sourceRole, orgTree, readOnly = false, teamStatus, attachments, targetRoles, presetId } = config;
|
|
411
411
|
|
|
412
412
|
// Note: Claude CLI doesn't support inline image attachments.
|
|
413
413
|
// Images will be ignored with a warning if passed.
|
|
@@ -416,7 +416,7 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
// 1. Context Assembly
|
|
419
|
-
const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus, targetRoles });
|
|
419
|
+
const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus, targetRoles, presetId });
|
|
420
420
|
|
|
421
421
|
// Trace: capture assembled prompt for debugging
|
|
422
422
|
callbacks.onPromptAssembled?.(context.systemPrompt, task);
|
|
@@ -45,6 +45,8 @@ export interface RunnerConfig {
|
|
|
45
45
|
codeRoot?: string;
|
|
46
46
|
/** PSM-004: Environment variables to inject (e.g., port assignments) */
|
|
47
47
|
env?: Record<string, string>;
|
|
48
|
+
/** Wave-scoped preset ID for knowledge injection */
|
|
49
|
+
presetId?: string;
|
|
48
50
|
/** SV-7: Supervision — abort a running session */
|
|
49
51
|
onAbortSession?: (sessionId: string) => boolean;
|
|
50
52
|
/** SV-6: Supervision — amend a running session */
|
|
@@ -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
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { COMPANY_ROOT } from './file-reader.js';
|
|
2
4
|
import { ActivityStream, type ActivityEvent } from './activity-stream.js';
|
|
3
5
|
import { buildOrgTree } from '../engine/org-tree.js';
|
|
@@ -116,7 +118,20 @@ class ExecutionManager {
|
|
|
116
118
|
|
|
117
119
|
startExecution(params: StartExecutionParams): Execution {
|
|
118
120
|
const execId = `exec-${Date.now()}-${this.nextId++}`;
|
|
119
|
-
|
|
121
|
+
|
|
122
|
+
// Resolve preset from wave file for org tree building
|
|
123
|
+
let presetId: string | undefined;
|
|
124
|
+
const session = getSession(params.sessionId);
|
|
125
|
+
if (session?.waveId) {
|
|
126
|
+
try {
|
|
127
|
+
const wavePath = path.join(COMPANY_ROOT, 'operations', 'waves', `${session.waveId}.json`);
|
|
128
|
+
if (fs.existsSync(wavePath)) {
|
|
129
|
+
const waveData = JSON.parse(fs.readFileSync(wavePath, 'utf-8'));
|
|
130
|
+
presetId = waveData.preset;
|
|
131
|
+
}
|
|
132
|
+
} catch { /* ignore */ }
|
|
133
|
+
}
|
|
134
|
+
const orgTree = buildOrgTree(COMPANY_ROOT, presetId);
|
|
120
135
|
|
|
121
136
|
// Authority gate
|
|
122
137
|
if (params.sourceRole && params.sourceRole !== 'ceo') {
|
|
@@ -165,7 +180,7 @@ class ExecutionManager {
|
|
|
165
180
|
|
|
166
181
|
this.executions.set(execId, execution);
|
|
167
182
|
|
|
168
|
-
this.initializeAndRunExecution(execution, params, orgTree);
|
|
183
|
+
this.initializeAndRunExecution(execution, params, orgTree, presetId);
|
|
169
184
|
|
|
170
185
|
return execution;
|
|
171
186
|
}
|
|
@@ -174,6 +189,7 @@ class ExecutionManager {
|
|
|
174
189
|
execution: Execution,
|
|
175
190
|
params: StartExecutionParams,
|
|
176
191
|
orgTree: ReturnType<typeof buildOrgTree>,
|
|
192
|
+
presetId?: string,
|
|
177
193
|
): Promise<void> {
|
|
178
194
|
try {
|
|
179
195
|
const ports = await portRegistry.allocate(execution.sessionId || execution.id, params.roleId, params.task);
|
|
@@ -243,6 +259,7 @@ class ExecutionManager {
|
|
|
243
259
|
sessionId: params.sessionId,
|
|
244
260
|
teamStatus,
|
|
245
261
|
targetRoles: params.targetRoles,
|
|
262
|
+
presetId,
|
|
246
263
|
codeRoot: resolveCodeRoot(COMPANY_ROOT),
|
|
247
264
|
attachments: params.attachments,
|
|
248
265
|
env: {
|
|
@@ -676,7 +693,18 @@ class ExecutionManager {
|
|
|
676
693
|
if (runningChildren.length === 0) return;
|
|
677
694
|
|
|
678
695
|
// Only restart C-Level roles (CTO, CBO etc.)
|
|
679
|
-
|
|
696
|
+
// Resolve preset from wave file for correct org tree
|
|
697
|
+
let recoveryPresetId: string | undefined;
|
|
698
|
+
const deadSession = getSession(deadExecution.sessionId);
|
|
699
|
+
if (deadSession?.waveId) {
|
|
700
|
+
try {
|
|
701
|
+
const wp = path.join(COMPANY_ROOT, 'operations', 'waves', `${deadSession.waveId}.json`);
|
|
702
|
+
if (fs.existsSync(wp)) {
|
|
703
|
+
recoveryPresetId = JSON.parse(fs.readFileSync(wp, 'utf-8')).preset;
|
|
704
|
+
}
|
|
705
|
+
} catch { /* ignore */ }
|
|
706
|
+
}
|
|
707
|
+
const orgTree = buildOrgTree(COMPANY_ROOT, recoveryPresetId);
|
|
680
708
|
const node = orgTree.nodes.get(deadExecution.roleId);
|
|
681
709
|
if (!node || node.level !== 'c-level') return;
|
|
682
710
|
|
|
@@ -697,8 +725,8 @@ Your job: monitor progress, course-correct if needed, wait for completion, then
|
|
|
697
725
|
console.log(`[ExecMgr] Supervision recovery: ${deadExecution.roleId} died with ${runningChildren.length} running children. Restarting.`);
|
|
698
726
|
|
|
699
727
|
// Propagate waveId from the dead session
|
|
700
|
-
const
|
|
701
|
-
const waveId =
|
|
728
|
+
const deadSes = getSession(deadExecution.sessionId);
|
|
729
|
+
const waveId = deadSes?.waveId;
|
|
702
730
|
|
|
703
731
|
// Create new session for recovery
|
|
704
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
|
});
|
|
@@ -139,7 +139,7 @@ function readFilePreview(filePath: string, maxLines: number): string[] {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
const PanelModeInner: React.FC<PanelModeProps> = ({
|
|
143
143
|
tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
|
|
144
144
|
streamStatus, waveId, activeSessions, allSessions, companyRoot, waves,
|
|
145
145
|
focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
|
|
@@ -338,6 +338,19 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
|
|
|
338
338
|
? allSessions.filter(s => s.waveId === focusedWaveId).length
|
|
339
339
|
: 0;
|
|
340
340
|
|
|
341
|
+
// Read preset from wave file on disk
|
|
342
|
+
const wavePreset = useMemo(() => {
|
|
343
|
+
if (!focusedWaveId || !companyRoot) return null;
|
|
344
|
+
try {
|
|
345
|
+
const wavePath = path.join(companyRoot, 'operations', 'waves', `${focusedWaveId}.json`);
|
|
346
|
+
if (fs.existsSync(wavePath)) {
|
|
347
|
+
const data = JSON.parse(fs.readFileSync(wavePath, 'utf-8'));
|
|
348
|
+
return data.preset as string | undefined;
|
|
349
|
+
}
|
|
350
|
+
} catch { /* ignore */ }
|
|
351
|
+
return null;
|
|
352
|
+
}, [focusedWaveId, companyRoot]);
|
|
353
|
+
|
|
341
354
|
const leftWidth = 28;
|
|
342
355
|
|
|
343
356
|
return (
|
|
@@ -347,6 +360,9 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
|
|
|
347
360
|
<Box flexDirection="column" width={leftWidth}>
|
|
348
361
|
<Box paddingX={1}>
|
|
349
362
|
<Text color="green" bold>W{focusedWaveIndex}</Text>
|
|
363
|
+
{wavePreset && wavePreset !== 'default' && (
|
|
364
|
+
<Text color="magenta"> ({wavePreset})</Text>
|
|
365
|
+
)}
|
|
350
366
|
<Text color="gray"> </Text>
|
|
351
367
|
<Text color="white" wrap="truncate">
|
|
352
368
|
{focusedWave?.directive ? focusedWave.directive.slice(0, leftWidth - 6) : '(idle)'}
|
|
@@ -385,7 +401,7 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
|
|
|
385
401
|
<Text color="gray">{separatorStr}</Text>
|
|
386
402
|
|
|
387
403
|
{/* Right: Tabbed panel */}
|
|
388
|
-
<Box flexGrow={1} flexDirection="column"
|
|
404
|
+
<Box flexGrow={1} flexDirection="column">
|
|
389
405
|
{/* Tab bar */}
|
|
390
406
|
<Box paddingX={1} marginBottom={0}>
|
|
391
407
|
{(['stream', 'docs', 'info'] as RightTab[]).map(tab => (
|
|
@@ -505,6 +521,7 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
|
|
|
505
521
|
<Text bold color="cyan">Wave Info</Text>
|
|
506
522
|
<Text color="gray">{'\u2500'.repeat(40)}</Text>
|
|
507
523
|
<Text color="white">Wave: {focusedWave?.waveId ?? 'none'}</Text>
|
|
524
|
+
{wavePreset && <Text color="magenta">Preset: {wavePreset}</Text>}
|
|
508
525
|
<Text color="white">Directive: {focusedWave?.directive || '(idle)'}</Text>
|
|
509
526
|
<Text color="white">Sessions: {waveSessionCount}</Text>
|
|
510
527
|
<Text color="white">Files modified: {waveFileSet.size}</Text>
|
|
@@ -545,4 +562,6 @@ export const PanelMode: React.FC<PanelModeProps> = React.memo(({
|
|
|
545
562
|
</Box>
|
|
546
563
|
</Box>
|
|
547
564
|
);
|
|
548
|
-
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
export const PanelMode = React.memo(PanelModeInner);
|