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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.96-beta.42",
3
+ "version": "0.1.96-beta.44",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 [, exec] of sessionList) {
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
@@ -551,6 +551,7 @@ export const App: React.FC = () => {
551
551
  waveId={focusedWaveId}
552
552
  activeSessions={api.activeSessions}
553
553
  allSessions={api.sessions}
554
+ knowledgeDocs={api.knowledgeDocs}
554
555
  waves={waves}
555
556
  focusedWaveId={focusedWaveId}
556
557
  onMove={(dir) => {
@@ -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
- // Files created in this wave
149
- const waveFiles = useMemo(() => extractWaveFiles(events), [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));
155
+ }, [rightTab === 'docs' || rightTab === 'info' ? events.length : 0, rightTab]);
150
156
 
151
- // File preview for selected doc
152
- const selectedFile = waveFiles[docsIndex] ?? null;
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 (!selectedFile || rightTab !== 'docs') return [];
155
- return readFilePreview(selectedFile, 100);
156
- }, [selectedFile, rightTab]);
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' && docsScroll > 0) {
178
- setDocsScroll(s => Math.max(0, s - 3));
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
- setDocsScroll(s => s + 3);
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, waveFiles.length));
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' && selectedFile) {
203
- // Open in vim
265
+ if (rightTab === 'docs' && selectedDoc) {
204
266
  const editor = process.env.EDITOR || 'vim';
205
267
  try {
206
- execSync(`${editor} "${selectedFile}"`, { stdio: 'inherit' });
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
- const num = parseInt(input, 10);
218
- if (num >= 1 && num <= 9 && num <= waves.length) {
219
- onFocusWave(waves[num - 1].waveId);
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
- {waveFiles.length === 0 ? (
350
- <Text color="gray">No files created in this wave yet.</Text>
351
- ) : (
352
- <>
353
- {/* File list */}
354
- <Box marginBottom={1}>
355
- <Text color="gray">Files ({waveFiles.length}): </Text>
356
- {waveFiles.map((f, i) => (
357
- <Box key={f} marginRight={1}>
358
- <Text
359
- color={i === docsIndex ? 'cyan' : 'gray'}
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
- {/* File preview */}
371
- {selectedFile && (
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>{selectedFile.split('/').slice(-2).join('/')}</Text>
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 - 12).map((line, i) => (
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={1}>
387
- <Text color="gray" dimColor>[Enter] open in {process.env.EDITOR || 'vim'} | [Tab] next file | [j/k] scroll</Text>
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: {waveFiles.length}</Text>
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 */}
@@ -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
  }