tycono 0.1.96-beta.42 → 0.1.96-beta.44
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
|
@@ -125,7 +125,7 @@ class WaveMultiplexer {
|
|
|
125
125
|
console.log(`[WaveMux] Replayed ${replayEvents.length} events (${sessionList.length} total sessions, ${recentSessions.length} replayed)`);
|
|
126
126
|
|
|
127
127
|
// Phase 2: Subscribe to live events for active sessions
|
|
128
|
-
for (const
|
|
128
|
+
for (const exec of sessionList) {
|
|
129
129
|
if (exec.status === 'running' || exec.status === 'awaiting_input') {
|
|
130
130
|
this.subscribeSessionToClient(waveId, client, exec, true);
|
|
131
131
|
}
|
package/src/tui/api.ts
CHANGED
|
@@ -189,6 +189,22 @@ export async function cleanupSessions(): Promise<{ cleaned: number; remaining: n
|
|
|
189
189
|
return fetchJson<{ cleaned: number; remaining: number }>('/api/active-sessions/cleanup', { method: 'POST' });
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/* ─── Knowledge docs ─── */
|
|
193
|
+
|
|
194
|
+
export interface KnowledgeDoc {
|
|
195
|
+
id: string;
|
|
196
|
+
title: string;
|
|
197
|
+
path: string;
|
|
198
|
+
type?: string;
|
|
199
|
+
domain?: string;
|
|
200
|
+
status?: string;
|
|
201
|
+
updatedAt?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function fetchKnowledgeDocs(): Promise<KnowledgeDoc[]> {
|
|
205
|
+
return fetchJson<KnowledgeDoc[]>('/api/knowledge');
|
|
206
|
+
}
|
|
207
|
+
|
|
192
208
|
/* ─── Setup API calls ─── */
|
|
193
209
|
|
|
194
210
|
export interface TeamTemplate {
|
package/src/tui/app.tsx
CHANGED
|
@@ -130,12 +130,14 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
|
|
|
130
130
|
else if (inp.pattern) detail = ` ${String(inp.pattern)}`;
|
|
131
131
|
else if (inp.description) detail = ` ${String(inp.description).slice(0, 60)}`;
|
|
132
132
|
}
|
|
133
|
+
// Highlight file writes
|
|
134
|
+
const isWrite = ['Write', 'Edit'].includes(toolName);
|
|
133
135
|
return {
|
|
134
136
|
id: ++lineCounter,
|
|
135
137
|
prefix: isSupervisor ? undefined : event.roleId,
|
|
136
138
|
prefixColor: roleColor,
|
|
137
|
-
text: ` \u2192 ${toolName}${detail}`,
|
|
138
|
-
color: 'gray',
|
|
139
|
+
text: isWrite ? ` \u{1F4C4} ${toolName}${detail}` : ` \u2192 ${toolName}${detail}`,
|
|
140
|
+
color: isWrite ? 'green' : 'gray',
|
|
139
141
|
indent: !isSupervisor,
|
|
140
142
|
};
|
|
141
143
|
}
|
|
@@ -19,10 +19,11 @@ import { execSync } from 'node:child_process';
|
|
|
19
19
|
import { OrgTree } from './OrgTree';
|
|
20
20
|
import { StreamView } from './StreamView';
|
|
21
21
|
import type { OrgNode } from '../store';
|
|
22
|
-
import type { SSEEvent, ActiveSessionInfo, SessionInfo } from '../api';
|
|
22
|
+
import type { SSEEvent, ActiveSessionInfo, SessionInfo, KnowledgeDoc } from '../api';
|
|
23
23
|
import type { WaveInfo } from '../hooks/useCommand';
|
|
24
24
|
|
|
25
25
|
type RightTab = 'stream' | 'docs' | 'info';
|
|
26
|
+
type DocsFilter = 'all' | 'wave' | 'kb' | 'projects';
|
|
26
27
|
|
|
27
28
|
interface PanelModeProps {
|
|
28
29
|
tree: OrgNode[];
|
|
@@ -34,6 +35,7 @@ interface PanelModeProps {
|
|
|
34
35
|
waveId: string | null;
|
|
35
36
|
activeSessions: ActiveSessionInfo[];
|
|
36
37
|
allSessions: SessionInfo[];
|
|
38
|
+
knowledgeDocs: KnowledgeDoc[];
|
|
37
39
|
waves: WaveInfo[];
|
|
38
40
|
focusedWaveId: string | null;
|
|
39
41
|
onMove: (direction: 'up' | 'down') => void;
|
|
@@ -113,11 +115,12 @@ function readFilePreview(filePath: string, maxLines: number): string[] {
|
|
|
113
115
|
|
|
114
116
|
export const PanelMode: React.FC<PanelModeProps> = ({
|
|
115
117
|
tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
|
|
116
|
-
streamStatus, waveId, activeSessions, allSessions, waves,
|
|
118
|
+
streamStatus, waveId, activeSessions, allSessions, knowledgeDocs, waves,
|
|
117
119
|
focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
|
|
118
120
|
}) => {
|
|
119
121
|
const [termHeight, setTermHeight] = useState(process.stdout.rows || 30);
|
|
120
122
|
const [rightTab, setRightTab] = useState<RightTab>('stream');
|
|
123
|
+
const [docsFilter, setDocsFilter] = useState<DocsFilter>('all');
|
|
121
124
|
const [docsIndex, setDocsIndex] = useState(0);
|
|
122
125
|
const [docsScroll, setDocsScroll] = useState(0);
|
|
123
126
|
|
|
@@ -145,15 +148,55 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
145
148
|
return tree.map(scopeNode);
|
|
146
149
|
}, [tree, waveScopedStatuses]);
|
|
147
150
|
|
|
148
|
-
//
|
|
149
|
-
const
|
|
151
|
+
// Wave files (from SSE events) — only compute when needed
|
|
152
|
+
const waveFileSet = useMemo(() => {
|
|
153
|
+
if (rightTab !== 'docs' && rightTab !== 'info') return new Set<string>();
|
|
154
|
+
return new Set(extractWaveFiles(events));
|
|
155
|
+
}, [rightTab === 'docs' || rightTab === 'info' ? events.length : 0, rightTab]);
|
|
150
156
|
|
|
151
|
-
//
|
|
152
|
-
const
|
|
157
|
+
// Build docs list based on filter
|
|
158
|
+
const docsList = useMemo(() => {
|
|
159
|
+
if (rightTab !== 'docs') return [];
|
|
160
|
+
|
|
161
|
+
interface DocEntry { path: string; title: string; isWave: boolean; }
|
|
162
|
+
const entries: DocEntry[] = [];
|
|
163
|
+
|
|
164
|
+
// KB docs from API
|
|
165
|
+
for (const doc of knowledgeDocs) {
|
|
166
|
+
const isWave = waveFileSet.has(doc.path);
|
|
167
|
+
const isKb = doc.id.startsWith('knowledge/');
|
|
168
|
+
const isProject = doc.id.startsWith('projects/');
|
|
169
|
+
|
|
170
|
+
if (docsFilter === 'wave' && !isWave) continue;
|
|
171
|
+
if (docsFilter === 'kb' && !isKb) continue;
|
|
172
|
+
if (docsFilter === 'projects' && !isProject) continue;
|
|
173
|
+
|
|
174
|
+
entries.push({ path: doc.path, title: doc.title || doc.id.split('/').pop() || '', isWave });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Wave-only files not in KB (e.g. code files)
|
|
178
|
+
for (const f of waveFileSet) {
|
|
179
|
+
if (!entries.some(e => e.path === f)) {
|
|
180
|
+
if (docsFilter === 'kb' || docsFilter === 'projects') continue;
|
|
181
|
+
entries.push({ path: f, title: f.split('/').pop() || f, isWave: true });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Sort: wave files first, then alphabetical
|
|
186
|
+
entries.sort((a, b) => {
|
|
187
|
+
if (a.isWave && !b.isWave) return -1;
|
|
188
|
+
if (!a.isWave && b.isWave) return 1;
|
|
189
|
+
return a.title.localeCompare(b.title);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return entries;
|
|
193
|
+
}, [rightTab, docsFilter, knowledgeDocs, waveFileSet]);
|
|
194
|
+
|
|
195
|
+
const selectedDoc = docsList[docsIndex] ?? null;
|
|
153
196
|
const filePreview = useMemo(() => {
|
|
154
|
-
if (!
|
|
155
|
-
return readFilePreview(
|
|
156
|
-
}, [
|
|
197
|
+
if (!selectedDoc || rightTab !== 'docs') return [];
|
|
198
|
+
return readFilePreview(selectedDoc.path, 60);
|
|
199
|
+
}, [selectedDoc?.path, rightTab]);
|
|
157
200
|
|
|
158
201
|
useInput((input, key) => {
|
|
159
202
|
if (key.escape) { onEscape(); return; }
|
|
@@ -174,8 +217,12 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
174
217
|
|
|
175
218
|
// j/k: context-dependent
|
|
176
219
|
if (key.upArrow || input === 'k') {
|
|
177
|
-
if (rightTab === 'docs'
|
|
178
|
-
|
|
220
|
+
if (rightTab === 'docs') {
|
|
221
|
+
if (docsScroll > 0) {
|
|
222
|
+
setDocsScroll(s => Math.max(0, s - 3));
|
|
223
|
+
} else {
|
|
224
|
+
setDocsIndex(i => Math.max(0, i - 1));
|
|
225
|
+
}
|
|
179
226
|
} else if (rightTab === 'stream') {
|
|
180
227
|
onMove('up');
|
|
181
228
|
}
|
|
@@ -183,28 +230,44 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
183
230
|
}
|
|
184
231
|
if (key.downArrow || input === 'j') {
|
|
185
232
|
if (rightTab === 'docs') {
|
|
186
|
-
|
|
233
|
+
if (docsScroll > 0) {
|
|
234
|
+
setDocsScroll(s => s + 3);
|
|
235
|
+
} else {
|
|
236
|
+
setDocsIndex(i => Math.min(docsList.length - 1, i + 1));
|
|
237
|
+
}
|
|
187
238
|
} else if (rightTab === 'stream') {
|
|
188
239
|
onMove('down');
|
|
189
240
|
}
|
|
190
241
|
return;
|
|
191
242
|
}
|
|
192
243
|
|
|
244
|
+
// Docs filter: 1-4
|
|
245
|
+
if (rightTab === 'docs') {
|
|
246
|
+
const filters: DocsFilter[] = ['all', 'wave', 'kb', 'projects'];
|
|
247
|
+
const fi = parseInt(input, 10);
|
|
248
|
+
if (fi >= 1 && fi <= 4) {
|
|
249
|
+
setDocsFilter(filters[fi - 1]);
|
|
250
|
+
setDocsIndex(0);
|
|
251
|
+
setDocsScroll(0);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
193
256
|
// Tab key for cycling docs files
|
|
194
257
|
if (key.tab && rightTab === 'docs') {
|
|
195
|
-
setDocsIndex(i => (i + 1) % Math.max(1,
|
|
258
|
+
setDocsIndex(i => (i + 1) % Math.max(1, docsList.length));
|
|
196
259
|
setDocsScroll(0);
|
|
197
260
|
return;
|
|
198
261
|
}
|
|
199
262
|
|
|
200
263
|
// Enter
|
|
201
264
|
if (key.return) {
|
|
202
|
-
if (rightTab === 'docs' &&
|
|
203
|
-
// Open in vim
|
|
265
|
+
if (rightTab === 'docs' && selectedDoc) {
|
|
204
266
|
const editor = process.env.EDITOR || 'vim';
|
|
205
267
|
try {
|
|
206
|
-
execSync(`${editor} "${
|
|
268
|
+
execSync(`${editor} "${selectedDoc.path}"`, { stdio: 'inherit' });
|
|
207
269
|
} catch { /* user quit editor */ }
|
|
270
|
+
fileCache.delete(selectedDoc.path); // Invalidate cache after edit
|
|
208
271
|
return;
|
|
209
272
|
}
|
|
210
273
|
if (rightTab === 'stream') {
|
|
@@ -213,10 +276,12 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
213
276
|
}
|
|
214
277
|
}
|
|
215
278
|
|
|
216
|
-
// 1-9: wave switch
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
279
|
+
// 1-9: wave switch (only in stream/info tabs — docs uses 1-4 for filters)
|
|
280
|
+
if (rightTab !== 'docs') {
|
|
281
|
+
const num = parseInt(input, 10);
|
|
282
|
+
if (num >= 1 && num <= 9 && num <= waves.length) {
|
|
283
|
+
onFocusWave(waves[num - 1].waveId);
|
|
284
|
+
}
|
|
220
285
|
}
|
|
221
286
|
});
|
|
222
287
|
|
|
@@ -343,50 +408,66 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
343
408
|
</>
|
|
344
409
|
)}
|
|
345
410
|
|
|
346
|
-
{/* Docs tab */}
|
|
411
|
+
{/* Docs tab — KB browser + wave artifacts */}
|
|
347
412
|
{rightTab === 'docs' && (
|
|
348
413
|
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
349
|
-
{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
bold={i === docsIndex}
|
|
361
|
-
inverse={i === docsIndex}
|
|
362
|
-
>
|
|
363
|
-
{` ${f.split('/').pop()} `}
|
|
364
|
-
</Text>
|
|
365
|
-
</Box>
|
|
366
|
-
))}
|
|
367
|
-
<Text color="gray" dimColor> [Tab] next</Text>
|
|
414
|
+
{/* Filter bar */}
|
|
415
|
+
<Box marginBottom={0}>
|
|
416
|
+
{(['all', 'wave', 'kb', 'projects'] as DocsFilter[]).map((f, i) => (
|
|
417
|
+
<Box key={f} marginRight={1}>
|
|
418
|
+
<Text
|
|
419
|
+
color={docsFilter === f ? 'cyan' : 'gray'}
|
|
420
|
+
bold={docsFilter === f}
|
|
421
|
+
inverse={docsFilter === f}
|
|
422
|
+
>
|
|
423
|
+
{f === 'wave' ? ` ${i + 1}:\u2605Wave ` : ` ${i + 1}:${f.charAt(0).toUpperCase() + f.slice(1)} `}
|
|
424
|
+
</Text>
|
|
368
425
|
</Box>
|
|
426
|
+
))}
|
|
427
|
+
<Text color="gray" dimColor> ({docsList.length})</Text>
|
|
428
|
+
</Box>
|
|
369
429
|
|
|
370
|
-
|
|
371
|
-
|
|
430
|
+
{docsList.length === 0 ? (
|
|
431
|
+
<Box marginTop={1}>
|
|
432
|
+
<Text color="gray">{docsFilter === 'wave' ? 'No files created in this wave.' : 'No documents found.'}</Text>
|
|
433
|
+
</Box>
|
|
434
|
+
) : (
|
|
435
|
+
<Box flexGrow={1} flexDirection="column">
|
|
436
|
+
{/* File list (scrollable region) */}
|
|
437
|
+
{!selectedDoc || docsScroll === 0 ? (
|
|
438
|
+
<Box flexDirection="column" marginTop={0}>
|
|
439
|
+
{docsList.slice(0, termHeight - 8).map((doc, i) => (
|
|
440
|
+
<Box key={doc.path}>
|
|
441
|
+
<Text
|
|
442
|
+
color={i === docsIndex ? 'cyan' : doc.isWave ? 'green' : 'white'}
|
|
443
|
+
bold={i === docsIndex}
|
|
444
|
+
inverse={i === docsIndex}
|
|
445
|
+
>
|
|
446
|
+
{doc.isWave ? '\u2605' : ' '} {doc.title.slice(0, 50)}
|
|
447
|
+
</Text>
|
|
448
|
+
</Box>
|
|
449
|
+
))}
|
|
450
|
+
{docsList.length > termHeight - 8 && (
|
|
451
|
+
<Text color="gray"> ... +{docsList.length - (termHeight - 8)} more (Tab to cycle)</Text>
|
|
452
|
+
)}
|
|
453
|
+
</Box>
|
|
454
|
+
) : (
|
|
455
|
+
/* File preview (when scrolled into file) */
|
|
372
456
|
<Box flexDirection="column">
|
|
373
|
-
<Text color="cyan" bold>{
|
|
457
|
+
<Text color="cyan" bold>{selectedDoc.isWave ? '\u2605 ' : ''}{selectedDoc.path.split('/').slice(-2).join('/')}</Text>
|
|
374
458
|
<Text color="gray">{'\u2500'.repeat(50)}</Text>
|
|
375
|
-
{filePreview.slice(docsScroll, docsScroll + termHeight -
|
|
459
|
+
{filePreview.slice(docsScroll - 1, docsScroll - 1 + termHeight - 10).map((line, i) => (
|
|
376
460
|
<Text key={i} color="white" wrap="wrap">{line}</Text>
|
|
377
461
|
))}
|
|
378
|
-
{filePreview.length > termHeight - 12 && (
|
|
379
|
-
<Text color="gray" dimColor>
|
|
380
|
-
{docsScroll > 0 ? '\u2191 ' : ''}j/k scroll | {filePreview.length - docsScroll} lines remaining
|
|
381
|
-
</Text>
|
|
382
|
-
)}
|
|
383
462
|
</Box>
|
|
384
463
|
)}
|
|
385
464
|
|
|
386
|
-
<Box marginTop={
|
|
387
|
-
<Text color="gray" dimColor>
|
|
465
|
+
<Box marginTop={0}>
|
|
466
|
+
<Text color="gray" dimColor>
|
|
467
|
+
[Enter] {process.env.EDITOR || 'vim'} | [Tab] next | [j/k] {docsScroll > 0 ? 'scroll' : 'select'} | [k] back
|
|
468
|
+
</Text>
|
|
388
469
|
</Box>
|
|
389
|
-
|
|
470
|
+
</Box>
|
|
390
471
|
)}
|
|
391
472
|
</Box>
|
|
392
473
|
)}
|
|
@@ -399,7 +480,7 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
399
480
|
<Text color="white">Wave: {focusedWave?.waveId ?? 'none'}</Text>
|
|
400
481
|
<Text color="white">Directive: {focusedWave?.directive || '(idle)'}</Text>
|
|
401
482
|
<Text color="white">Sessions: {waveSessionCount}</Text>
|
|
402
|
-
<Text color="white">Files modified: {
|
|
483
|
+
<Text color="white">Files modified: {waveFileSet.size}</Text>
|
|
403
484
|
<Text color="white">SSE events: {events.length}</Text>
|
|
404
485
|
|
|
405
486
|
{/* Active sessions in this wave */}
|
package/src/tui/hooks/useApi.ts
CHANGED
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
fetchExecStatus,
|
|
10
10
|
fetchActiveWaves,
|
|
11
11
|
fetchActiveSessions,
|
|
12
|
+
fetchKnowledgeDocs,
|
|
12
13
|
type CompanyInfo,
|
|
13
14
|
type SessionInfo,
|
|
14
15
|
type ExecStatus,
|
|
15
16
|
type ActiveSessionInfo,
|
|
17
|
+
type KnowledgeDoc,
|
|
16
18
|
} from '../api';
|
|
17
19
|
|
|
18
20
|
const POLL_INTERVAL = 5000; // 5 seconds (reduce re-renders)
|
|
@@ -31,6 +33,7 @@ export interface ApiState {
|
|
|
31
33
|
activeWaves: ActiveWaveInfo[];
|
|
32
34
|
activeSessions: ActiveSessionInfo[];
|
|
33
35
|
portSummary: { active: number; totalPorts: number };
|
|
36
|
+
knowledgeDocs: KnowledgeDoc[];
|
|
34
37
|
error: string | null;
|
|
35
38
|
loaded: boolean;
|
|
36
39
|
refresh(): void;
|
|
@@ -43,6 +46,8 @@ export function useApi(): ApiState {
|
|
|
43
46
|
const [activeWaves, setActiveWaves] = useState<ActiveWaveInfo[]>([]);
|
|
44
47
|
const [activeSessions, setActiveSessions] = useState<ActiveSessionInfo[]>([]);
|
|
45
48
|
const [portSummary, setPortSummary] = useState<{ active: number; totalPorts: number }>({ active: 0, totalPorts: 0 });
|
|
49
|
+
const [knowledgeDocs, setKnowledgeDocs] = useState<KnowledgeDoc[]>([]);
|
|
50
|
+
const kbLoadedRef = useRef(false);
|
|
46
51
|
const [error, setError] = useState<string | null>(null);
|
|
47
52
|
const [loaded, setLoaded] = useState(false);
|
|
48
53
|
const mountedRef = useRef(true);
|
|
@@ -75,6 +80,14 @@ export function useApi(): ApiState {
|
|
|
75
80
|
setActiveSessions(activeSess.sessions ?? []);
|
|
76
81
|
setPortSummary(activeSess.summary ?? { active: 0, totalPorts: 0 });
|
|
77
82
|
|
|
83
|
+
// KB docs (load once, not every poll)
|
|
84
|
+
if (!kbLoadedRef.current) {
|
|
85
|
+
kbLoadedRef.current = true;
|
|
86
|
+
fetchKnowledgeDocs().then(docs => {
|
|
87
|
+
if (mountedRef.current) setKnowledgeDocs(docs);
|
|
88
|
+
}).catch(() => {});
|
|
89
|
+
}
|
|
90
|
+
|
|
78
91
|
setError(null);
|
|
79
92
|
setLoaded(true);
|
|
80
93
|
} catch (err) {
|
|
@@ -94,5 +107,5 @@ export function useApi(): ApiState {
|
|
|
94
107
|
};
|
|
95
108
|
}, [refresh]);
|
|
96
109
|
|
|
97
|
-
return { company, sessions, execStatus, activeWaves, activeSessions, portSummary, error, loaded, refresh };
|
|
110
|
+
return { company, sessions, execStatus, activeWaves, activeSessions, portSummary, knowledgeDocs, error, loaded, refresh };
|
|
98
111
|
}
|