tycono 0.1.96-beta.56 → 0.1.96-beta.58

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.56",
3
+ "version": "0.1.96-beta.58",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/api.ts CHANGED
@@ -208,6 +208,12 @@ export async function fetchKnowledgeDocs(): Promise<KnowledgeDoc[]> {
208
208
  return fetchJson<KnowledgeDoc[]>('/api/knowledge');
209
209
  }
210
210
 
211
+ /** Scan COMPANY_ROOT for all .md files (not API-dependent) */
212
+ export async function fetchCompanyRoot(): Promise<string> {
213
+ const health = await fetchJson<{ companyRoot: string }>('/api/health');
214
+ return health.companyRoot;
215
+ }
216
+
211
217
  /* ─── Setup API calls ─── */
212
218
 
213
219
  export interface TeamTemplate {
package/src/tui/app.tsx CHANGED
@@ -563,7 +563,7 @@ export const App: React.FC = () => {
563
563
  waveId={focusedWaveId}
564
564
  activeSessions={api.activeSessions}
565
565
  allSessions={api.sessions}
566
- knowledgeDocs={api.knowledgeDocs}
566
+ companyRoot={process.env.COMPANY_ROOT || process.cwd()}
567
567
  waves={waves}
568
568
  focusedWaveId={focusedWaveId}
569
569
  onMove={(dir) => {
@@ -19,7 +19,8 @@ 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, KnowledgeDoc } from '../api';
22
+ import path from 'node:path';
23
+ import type { SSEEvent, ActiveSessionInfo, SessionInfo } from '../api';
23
24
  import type { WaveInfo } from '../hooks/useCommand';
24
25
 
25
26
  type RightTab = 'stream' | 'docs' | 'info';
@@ -35,7 +36,7 @@ interface PanelModeProps {
35
36
  waveId: string | null;
36
37
  activeSessions: ActiveSessionInfo[];
37
38
  allSessions: SessionInfo[];
38
- knowledgeDocs: KnowledgeDoc[];
39
+ companyRoot: string;
39
40
  waves: WaveInfo[];
40
41
  focusedWaveId: string | null;
41
42
  onMove: (direction: 'up' | 'down') => void;
@@ -78,6 +79,31 @@ function elapsed(startedAt: string): string {
78
79
  return `${Math.floor(ms / 3600_000)}h`;
79
80
  }
80
81
 
82
+ /** Scan COMPANY_ROOT for .md files (cached) */
83
+ let mdFileCache: { root: string; files: string[] } | null = null;
84
+ function scanMdFiles(companyRoot: string): string[] {
85
+ if (mdFileCache && mdFileCache.root === companyRoot) return mdFileCache.files;
86
+ const results: string[] = [];
87
+ const skip = new Set(['.git', 'node_modules', '.tycono', '.worktrees', 'dist', '.claude']);
88
+ function walk(dir: string, depth: number) {
89
+ if (depth > 3) return; // Don't go too deep
90
+ try {
91
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
92
+ if (skip.has(entry.name)) continue;
93
+ const full = path.join(dir, entry.name);
94
+ if (entry.isDirectory()) {
95
+ walk(full, depth + 1);
96
+ } else if (entry.name.endsWith('.md')) {
97
+ results.push(full);
98
+ }
99
+ }
100
+ } catch { /* permission error etc */ }
101
+ }
102
+ walk(companyRoot, 0);
103
+ mdFileCache = { root: companyRoot, files: results };
104
+ return results;
105
+ }
106
+
81
107
  /** Extract files created/modified in this wave from SSE events */
82
108
  function extractWaveFiles(events: SSEEvent[]): string[] {
83
109
  const files = new Set<string>();
@@ -115,7 +141,7 @@ function readFilePreview(filePath: string, maxLines: number): string[] {
115
141
 
116
142
  export const PanelMode: React.FC<PanelModeProps> = ({
117
143
  tree, flatRoles, events, selectedRoleIndex, selectedRoleId,
118
- streamStatus, waveId, activeSessions, allSessions, knowledgeDocs, waves,
144
+ streamStatus, waveId, activeSessions, allSessions, companyRoot, waves,
119
145
  focusedWaveId, onMove, onSelect, onEscape, onFocusWave,
120
146
  }) => {
121
147
  const [termHeight, setTermHeight] = useState(process.stdout.rows || 30);
@@ -154,27 +180,30 @@ export const PanelMode: React.FC<PanelModeProps> = ({
154
180
  return new Set(extractWaveFiles(events));
155
181
  }, [rightTab === 'docs' || rightTab === 'info' ? events.length : 0, rightTab]);
156
182
 
157
- // Build docs list based on filter
183
+ // Build docs list from filesystem scan + wave files
158
184
  const docsList = useMemo(() => {
159
185
  if (rightTab !== 'docs') return [];
160
186
 
161
187
  interface DocEntry { path: string; title: string; isWave: boolean; }
162
188
  const entries: DocEntry[] = [];
163
189
 
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/');
190
+ // Scan all .md files from COMPANY_ROOT
191
+ const allMdFiles = companyRoot ? scanMdFiles(companyRoot) : [];
192
+
193
+ for (const filePath of allMdFiles) {
194
+ const rel = filePath.replace(companyRoot + '/', '');
195
+ const isWave = waveFileSet.has(filePath);
196
+ const isKb = rel.startsWith('knowledge/');
197
+ const isProject = rel.startsWith('projects/');
169
198
 
170
199
  if (docsFilter === 'wave' && !isWave) continue;
171
200
  if (docsFilter === 'kb' && !isKb) continue;
172
201
  if (docsFilter === 'projects' && !isProject) continue;
173
202
 
174
- entries.push({ path: doc.path, title: doc.title || doc.id.split('/').pop() || '', isWave });
203
+ entries.push({ path: filePath, title: rel, isWave });
175
204
  }
176
205
 
177
- // Wave-only files not in KB (e.g. code files)
206
+ // Wave-only files not already in list (e.g. code files written by agents)
178
207
  for (const f of waveFileSet) {
179
208
  if (!entries.some(e => e.path === f)) {
180
209
  if (docsFilter === 'kb' || docsFilter === 'projects') continue;
@@ -190,7 +219,7 @@ export const PanelMode: React.FC<PanelModeProps> = ({
190
219
  });
191
220
 
192
221
  return entries;
193
- }, [rightTab, docsFilter, knowledgeDocs, waveFileSet]);
222
+ }, [rightTab, docsFilter, companyRoot, waveFileSet]);
194
223
 
195
224
  const selectedDoc = docsList[docsIndex] ?? null;
196
225
  const filePreview = useMemo(() => {
@@ -35,19 +35,29 @@ export const SetupWizard: React.FC<SetupWizardProps> = ({ onComplete }) => {
35
35
  const [resultRoles, setResultRoles] = useState(0);
36
36
  const [errorMsg, setErrorMsg] = useState('');
37
37
 
38
- // Load team templates delay to let Ink's first render complete
39
- // Without delay, Ink's synchronous render blocks the event loop
40
- // and http response callbacks don't fire → timeout
38
+ // Load team templates with retry same-process server+client
39
+ // can have event loop contention causing ECONNRESET or timeout
41
40
  useEffect(() => {
42
- const timer = setTimeout(() => {
43
- fetchSetupTeams()
44
- .then(setTeams)
45
- .catch((err) => {
46
- setErrorMsg(`Failed to load team templates: ${err.message}`);
47
- setStep('error');
48
- });
49
- }, 500);
50
- return () => clearTimeout(timer);
41
+ let cancelled = false;
42
+ const load = async (attempt = 0): Promise<void> => {
43
+ if (cancelled) return;
44
+ // Delay to let Ink's render cycle settle
45
+ await new Promise(r => setTimeout(r, attempt === 0 ? 1000 : 2000));
46
+ if (cancelled) return;
47
+ try {
48
+ const teams = await fetchSetupTeams();
49
+ if (!cancelled) setTeams(teams);
50
+ } catch (err) {
51
+ if (cancelled) return;
52
+ if (attempt < 3) {
53
+ return load(attempt + 1);
54
+ }
55
+ setErrorMsg(`Failed to load team templates: ${err instanceof Error ? err.message : 'unknown'}`);
56
+ setStep('error');
57
+ }
58
+ };
59
+ load();
60
+ return () => { cancelled = true; };
51
61
  }, []);
52
62
 
53
63
  // Esc to cancel (only during input steps)