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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.3.14-beta.0",
3
+ "version": "0.3.14-beta.10",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 /api/presets — list all preset summaries
5
- * GET /api/presets/:id — get full preset detail
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 for TUI */
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
- const orgTree = buildOrgTree(COMPANY_ROOT);
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
- const orgTree = buildOrgTree(COMPANY_ROOT);
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 deadSession = getSession(deadExecution.sessionId);
701
- const waveId = deadSession?.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" height={termHeight}>
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 with real-time status
3
- * CEO is now selectable (index 0 in flatRoles)
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 statusColor(status: string): string {
20
- switch (status) {
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
- linePrefix += isLast[j] ? ' ' : '\u2502 ';
26
+ prefix += isLast[j] ? ' ' : '\u2502 ';
52
27
  }
53
- linePrefix += isLast.length > 0 || i > 0 || nodes.length > 1
54
- ? (last ? '\u2514\u2500 ' : '\u251C\u2500 ')
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, '', [...isLast, last]));
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 ceoColor = statusColor(ceoStatus ?? 'idle');
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
- <Box marginTop={1}>
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
- export const PanelMode: React.FC<PanelModeProps> = React.memo(({
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" overflow="hidden">
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);