tycono 0.1.96-beta.37 → 0.1.96-beta.39

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.37",
3
+ "version": "0.1.96-beta.39",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@ import { Box, Text, Static } from 'ink';
10
10
  import TextInput from 'ink-text-input';
11
11
  import type { SSEEvent } from '../api';
12
12
  import { getRoleColor } from '../theme';
13
+ // Markdown rendering is done inline via regex (no external dependency)
13
14
 
14
15
  const SUPERVISOR_ROLE = 'ceo';
15
16
 
@@ -20,6 +21,7 @@ export interface StreamLine {
20
21
  prefix?: string;
21
22
  prefixColor?: string;
22
23
  indent?: boolean;
24
+ markdown?: boolean; // render text as markdown
23
25
  }
24
26
 
25
27
  interface CommandModeProps {
@@ -56,6 +58,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
56
58
  id: ++lineCounter,
57
59
  text,
58
60
  color: 'white',
61
+ markdown: true,
59
62
  };
60
63
  } else {
61
64
  return {
@@ -65,6 +68,7 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
65
68
  text,
66
69
  color: 'white',
67
70
  indent: true,
71
+ markdown: true,
68
72
  };
69
73
  }
70
74
  }
@@ -232,6 +236,28 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
232
236
 
233
237
  /** Render a single StreamLine */
234
238
  function StreamLineRow({ line }: { line: StreamLine }) {
239
+ // Markdown: inline formatting only (no multi-line splitting — too many elements)
240
+ if (line.markdown) {
241
+ // Strip markdown markers for terminal display
242
+ const cleaned = line.text
243
+ .replace(/^#{1,4}\s+/gm, '') // ## heading → heading
244
+ .replace(/\*\*(.+?)\*\*/g, '$1') // **bold** → bold
245
+ .replace(/`(.+?)`/g, '$1') // `code` → code
246
+ .replace(/^---+$/gm, '\u2500'.repeat(40)); // --- → line
247
+
248
+ return (
249
+ <Box>
250
+ {line.indent && <Text> </Text>}
251
+ {line.prefix && (
252
+ <Text color={line.prefixColor} bold>
253
+ {(line.prefix).padEnd(12)}
254
+ </Text>
255
+ )}
256
+ <Text color={line.color} wrap="wrap">{cleaned}</Text>
257
+ </Box>
258
+ );
259
+ }
260
+
235
261
  return (
236
262
  <Box>
237
263
  {line.indent && <Text> </Text>}
@@ -261,19 +287,19 @@ export const CommandMode: React.FC<CommandModeProps> = ({
261
287
  if (line) eventLines.push(line);
262
288
  }
263
289
 
264
- // Merge system messages and event lines (cap total to prevent memory bloat)
265
- const allLines = [...systemMessages, ...eventLines].slice(-200);
290
+ // Merge system messages and event lines (cap total)
291
+ const allLines = [...systemMessages, ...eventLines].slice(-100);
266
292
 
267
293
  // Split into committed (scrollback) and live (re-rendered)
268
294
  const newCommitted = allLines.slice(committedRef.current);
269
- if (newCommitted.length > 8) {
270
- const toCommit = newCommitted.slice(0, -8);
295
+ if (newCommitted.length > 6) {
296
+ const toCommit = newCommitted.slice(0, -6);
271
297
  committedRef.current += toCommit.length;
272
298
  }
273
299
 
274
300
  // Cap committed to prevent Static from holding too many items
275
301
  const rawCommitted = allLines.slice(0, committedRef.current);
276
- const committedLines = rawCommitted.length > 100 ? rawCommitted.slice(-100) : rawCommitted;
302
+ const committedLines = rawCommitted.length > 50 ? rawCommitted.slice(-50) : rawCommitted;
277
303
  const liveLines = allLines.slice(committedRef.current);
278
304
 
279
305
  const handleSubmit = useCallback((value: string) => {
@@ -91,11 +91,21 @@ function extractWaveFiles(events: SSEEvent[]): string[] {
91
91
  return Array.from(files);
92
92
  }
93
93
 
94
- /** Read file preview (first N lines) */
94
+ /** Read file preview (first N lines, cached) */
95
+ const fileCache = new Map<string, string[]>();
95
96
  function readFilePreview(filePath: string, maxLines: number): string[] {
97
+ const cached = fileCache.get(filePath);
98
+ if (cached) return cached;
96
99
  try {
97
100
  const content = fs.readFileSync(filePath, 'utf-8');
98
- return content.split('\n').slice(0, maxLines);
101
+ const lines = content.split('\n').slice(0, maxLines);
102
+ fileCache.set(filePath, lines);
103
+ // Evict old entries
104
+ if (fileCache.size > 5) {
105
+ const first = fileCache.keys().next().value;
106
+ if (first) fileCache.delete(first);
107
+ }
108
+ return lines;
99
109
  } catch {
100
110
  return ['(cannot read file)'];
101
111
  }
@@ -8,7 +8,7 @@
8
8
  import { useState, useEffect, useRef, useCallback } from 'react';
9
9
  import { subscribeToWaveStream, type SSEEvent, type SSEConnection } from '../api';
10
10
 
11
- const MAX_EVENTS = 150;
11
+ const MAX_EVENTS = 100;
12
12
  const RECONNECT_DELAY_MS = 3000;
13
13
  const MAX_RECONNECT_DELAY_MS = 15000;
14
14
  const BATCH_INTERVAL_MS = 300; // Throttle: update React state max ~3x/sec
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Terminal Markdown Renderer
3
+ *
4
+ * Converts markdown text to Ink <Text> elements with basic formatting:
5
+ * - **bold** → bold text
6
+ * - `code` → dimmed text
7
+ * - ## heading → bold colored text
8
+ * - --- → horizontal line
9
+ * - | table | → kept as-is (monospace already works)
10
+ */
11
+
12
+ import React from 'react';
13
+ import { Text } from 'ink';
14
+
15
+ interface Segment {
16
+ text: string;
17
+ bold?: boolean;
18
+ dim?: boolean;
19
+ color?: string;
20
+ }
21
+
22
+ /** Parse inline markdown (bold, code) into segments */
23
+ function parseInline(text: string): Segment[] {
24
+ const segments: Segment[] = [];
25
+ let remaining = text;
26
+
27
+ while (remaining.length > 0) {
28
+ // Bold: **text**
29
+ const boldMatch = remaining.match(/^(.*?)\*\*(.+?)\*\*(.*)/s);
30
+ if (boldMatch) {
31
+ if (boldMatch[1]) segments.push({ text: boldMatch[1] });
32
+ segments.push({ text: boldMatch[2], bold: true });
33
+ remaining = boldMatch[3];
34
+ continue;
35
+ }
36
+
37
+ // Inline code: `text`
38
+ const codeMatch = remaining.match(/^(.*?)`(.+?)`(.*)/s);
39
+ if (codeMatch) {
40
+ if (codeMatch[1]) segments.push({ text: codeMatch[1] });
41
+ segments.push({ text: codeMatch[2], dim: true, color: 'yellow' });
42
+ remaining = codeMatch[3];
43
+ continue;
44
+ }
45
+
46
+ // No more matches — push rest
47
+ segments.push({ text: remaining });
48
+ break;
49
+ }
50
+
51
+ return segments;
52
+ }
53
+
54
+ /** Render a single line of markdown as Ink elements */
55
+ export function renderMarkdownLine(line: string, key: string | number): React.ReactElement {
56
+ // Horizontal rule
57
+ if (/^---+$/.test(line.trim())) {
58
+ return <Text key={key} color="gray">{'\u2500'.repeat(Math.min(60, process.stdout.columns || 60))}</Text>;
59
+ }
60
+
61
+ // Heading: ## text
62
+ const headingMatch = line.match(/^(#{1,4})\s+(.+)/);
63
+ if (headingMatch) {
64
+ const level = headingMatch[1].length;
65
+ const content = headingMatch[2].replace(/\*\*/g, ''); // Strip bold in headings
66
+ const color = level <= 2 ? 'cyan' : 'white';
67
+ return <Text key={key} color={color} bold>{content}</Text>;
68
+ }
69
+
70
+ // Empty line
71
+ if (!line.trim()) {
72
+ return <Text key={key}> </Text>;
73
+ }
74
+
75
+ // Regular line with inline formatting
76
+ const segments = parseInline(line);
77
+
78
+ if (segments.length === 1 && !segments[0].bold && !segments[0].dim) {
79
+ // Simple text — no formatting needed
80
+ return <Text key={key} color="white">{segments[0].text}</Text>;
81
+ }
82
+
83
+ return (
84
+ <Text key={key}>
85
+ {segments.map((seg, i) => (
86
+ <Text
87
+ key={i}
88
+ bold={seg.bold}
89
+ dimColor={seg.dim}
90
+ color={seg.color ?? 'white'}
91
+ >
92
+ {seg.text}
93
+ </Text>
94
+ ))}
95
+ </Text>
96
+ );
97
+ }
98
+
99
+ /** Render multi-line markdown text as array of Ink elements */
100
+ export function renderMarkdown(text: string, baseKey: string | number = 0): React.ReactElement[] {
101
+ return text.split('\n').map((line, i) => renderMarkdownLine(line, `${baseKey}-${i}`));
102
+ }