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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.96-beta.43",
3
+ "version": "0.1.96-beta.44",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
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,18 +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 — only compute when Docs tab is active
149
- const waveFiles = useMemo(() => {
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
- // File preview only when Docs tab + file selected
155
- 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;
156
196
  const filePreview = useMemo(() => {
157
- if (!selectedFile || rightTab !== 'docs') return [];
158
- return readFilePreview(selectedFile, 60);
159
- }, [selectedFile, rightTab]);
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' && docsScroll > 0) {
181
- 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
+ }
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
- 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
+ }
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, waveFiles.length));
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' && selectedFile) {
206
- // Open in vim
265
+ if (rightTab === 'docs' && selectedDoc) {
207
266
  const editor = process.env.EDITOR || 'vim';
208
267
  try {
209
- execSync(`${editor} "${selectedFile}"`, { stdio: 'inherit' });
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
- const num = parseInt(input, 10);
221
- if (num >= 1 && num <= 9 && num <= waves.length) {
222
- 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
+ }
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
- {waveFiles.length === 0 ? (
353
- <Text color="gray">No files created in this wave yet.</Text>
354
- ) : (
355
- <>
356
- {/* File list */}
357
- <Box marginBottom={1}>
358
- <Text color="gray">Files ({waveFiles.length}): </Text>
359
- {waveFiles.map((f, i) => (
360
- <Box key={f} marginRight={1}>
361
- <Text
362
- color={i === docsIndex ? 'cyan' : 'gray'}
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
- {/* File preview */}
374
- {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) */
375
456
  <Box flexDirection="column">
376
- <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>
377
458
  <Text color="gray">{'\u2500'.repeat(50)}</Text>
378
- {filePreview.slice(docsScroll, docsScroll + termHeight - 12).map((line, i) => (
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={1}>
390
- <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>
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: {waveFiles.length}</Text>
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 */}
@@ -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
  }