tycono 0.1.96-beta.43 → 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 +1 -1
- package/src/tui/api.ts +16 -0
- package/src/tui/app.tsx +1 -0
- package/src/tui/components/CommandMode.tsx +4 -2
- package/src/tui/components/PanelMode.tsx +133 -55
- package/src/tui/hooks/useApi.ts +14 -1
package/package.json
CHANGED
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,18 +148,55 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
145
148
|
return tree.map(scopeNode);
|
|
146
149
|
}, [tree, waveScopedStatuses]);
|
|
147
150
|
|
|
148
|
-
//
|
|
149
|
-
const
|
|
150
|
-
if (rightTab !== 'docs' && rightTab !== 'info') return
|
|
151
|
-
return extractWaveFiles(events);
|
|
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));
|
|
152
155
|
}, [rightTab === 'docs' || rightTab === 'info' ? events.length : 0, rightTab]);
|
|
153
156
|
|
|
154
|
-
//
|
|
155
|
-
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;
|
|
156
196
|
const filePreview = useMemo(() => {
|
|
157
|
-
if (!
|
|
158
|
-
return readFilePreview(
|
|
159
|
-
}, [
|
|
197
|
+
if (!selectedDoc || rightTab !== 'docs') return [];
|
|
198
|
+
return readFilePreview(selectedDoc.path, 60);
|
|
199
|
+
}, [selectedDoc?.path, rightTab]);
|
|
160
200
|
|
|
161
201
|
useInput((input, key) => {
|
|
162
202
|
if (key.escape) { onEscape(); return; }
|
|
@@ -177,8 +217,12 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
177
217
|
|
|
178
218
|
// j/k: context-dependent
|
|
179
219
|
if (key.upArrow || input === 'k') {
|
|
180
|
-
if (rightTab === 'docs'
|
|
181
|
-
|
|
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
|
+
}
|
|
182
226
|
} else if (rightTab === 'stream') {
|
|
183
227
|
onMove('up');
|
|
184
228
|
}
|
|
@@ -186,28 +230,44 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
186
230
|
}
|
|
187
231
|
if (key.downArrow || input === 'j') {
|
|
188
232
|
if (rightTab === 'docs') {
|
|
189
|
-
|
|
233
|
+
if (docsScroll > 0) {
|
|
234
|
+
setDocsScroll(s => s + 3);
|
|
235
|
+
} else {
|
|
236
|
+
setDocsIndex(i => Math.min(docsList.length - 1, i + 1));
|
|
237
|
+
}
|
|
190
238
|
} else if (rightTab === 'stream') {
|
|
191
239
|
onMove('down');
|
|
192
240
|
}
|
|
193
241
|
return;
|
|
194
242
|
}
|
|
195
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
|
+
|
|
196
256
|
// Tab key for cycling docs files
|
|
197
257
|
if (key.tab && rightTab === 'docs') {
|
|
198
|
-
setDocsIndex(i => (i + 1) % Math.max(1,
|
|
258
|
+
setDocsIndex(i => (i + 1) % Math.max(1, docsList.length));
|
|
199
259
|
setDocsScroll(0);
|
|
200
260
|
return;
|
|
201
261
|
}
|
|
202
262
|
|
|
203
263
|
// Enter
|
|
204
264
|
if (key.return) {
|
|
205
|
-
if (rightTab === 'docs' &&
|
|
206
|
-
// Open in vim
|
|
265
|
+
if (rightTab === 'docs' && selectedDoc) {
|
|
207
266
|
const editor = process.env.EDITOR || 'vim';
|
|
208
267
|
try {
|
|
209
|
-
execSync(`${editor} "${
|
|
268
|
+
execSync(`${editor} "${selectedDoc.path}"`, { stdio: 'inherit' });
|
|
210
269
|
} catch { /* user quit editor */ }
|
|
270
|
+
fileCache.delete(selectedDoc.path); // Invalidate cache after edit
|
|
211
271
|
return;
|
|
212
272
|
}
|
|
213
273
|
if (rightTab === 'stream') {
|
|
@@ -216,10 +276,12 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
216
276
|
}
|
|
217
277
|
}
|
|
218
278
|
|
|
219
|
-
// 1-9: wave switch
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
223
285
|
}
|
|
224
286
|
});
|
|
225
287
|
|
|
@@ -346,50 +408,66 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
346
408
|
</>
|
|
347
409
|
)}
|
|
348
410
|
|
|
349
|
-
{/* Docs tab */}
|
|
411
|
+
{/* Docs tab — KB browser + wave artifacts */}
|
|
350
412
|
{rightTab === 'docs' && (
|
|
351
413
|
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
352
|
-
{
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
bold={i === docsIndex}
|
|
364
|
-
inverse={i === docsIndex}
|
|
365
|
-
>
|
|
366
|
-
{` ${f.split('/').pop()} `}
|
|
367
|
-
</Text>
|
|
368
|
-
</Box>
|
|
369
|
-
))}
|
|
370
|
-
<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>
|
|
371
425
|
</Box>
|
|
426
|
+
))}
|
|
427
|
+
<Text color="gray" dimColor> ({docsList.length})</Text>
|
|
428
|
+
</Box>
|
|
372
429
|
|
|
373
|
-
|
|
374
|
-
|
|
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) */
|
|
375
456
|
<Box flexDirection="column">
|
|
376
|
-
<Text color="cyan" bold>{
|
|
457
|
+
<Text color="cyan" bold>{selectedDoc.isWave ? '\u2605 ' : ''}{selectedDoc.path.split('/').slice(-2).join('/')}</Text>
|
|
377
458
|
<Text color="gray">{'\u2500'.repeat(50)}</Text>
|
|
378
|
-
{filePreview.slice(docsScroll, docsScroll + termHeight -
|
|
459
|
+
{filePreview.slice(docsScroll - 1, docsScroll - 1 + termHeight - 10).map((line, i) => (
|
|
379
460
|
<Text key={i} color="white" wrap="wrap">{line}</Text>
|
|
380
461
|
))}
|
|
381
|
-
{filePreview.length > termHeight - 12 && (
|
|
382
|
-
<Text color="gray" dimColor>
|
|
383
|
-
{docsScroll > 0 ? '\u2191 ' : ''}j/k scroll | {filePreview.length - docsScroll} lines remaining
|
|
384
|
-
</Text>
|
|
385
|
-
)}
|
|
386
462
|
</Box>
|
|
387
463
|
)}
|
|
388
464
|
|
|
389
|
-
<Box marginTop={
|
|
390
|
-
<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>
|
|
391
469
|
</Box>
|
|
392
|
-
|
|
470
|
+
</Box>
|
|
393
471
|
)}
|
|
394
472
|
</Box>
|
|
395
473
|
)}
|
|
@@ -402,7 +480,7 @@ export const PanelMode: React.FC<PanelModeProps> = ({
|
|
|
402
480
|
<Text color="white">Wave: {focusedWave?.waveId ?? 'none'}</Text>
|
|
403
481
|
<Text color="white">Directive: {focusedWave?.directive || '(idle)'}</Text>
|
|
404
482
|
<Text color="white">Sessions: {waveSessionCount}</Text>
|
|
405
|
-
<Text color="white">Files modified: {
|
|
483
|
+
<Text color="white">Files modified: {waveFileSet.size}</Text>
|
|
406
484
|
<Text color="white">SSE events: {events.length}</Text>
|
|
407
485
|
|
|
408
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
|
}
|