tycono 0.1.96-beta.37 → 0.1.96-beta.38

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.38",
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
+ import { renderMarkdownLine } from '../utils/markdown';
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,43 @@ export function summarizeEvent(event: SSEEvent, allRoleIds: string[]): StreamLin
232
236
 
233
237
  /** Render a single StreamLine */
234
238
  function StreamLineRow({ line }: { line: StreamLine }) {
239
+ // Multi-line markdown: split and render each line
240
+ if (line.markdown && line.text.includes('\n')) {
241
+ const lines = line.text.split('\n');
242
+ return (
243
+ <Box flexDirection="column">
244
+ {lines.map((l, i) => (
245
+ <Box key={i}>
246
+ {line.indent && <Text> </Text>}
247
+ {line.prefix && i === 0 && (
248
+ <Text color={line.prefixColor} bold>
249
+ {(line.prefix).padEnd(12)}
250
+ </Text>
251
+ )}
252
+ {line.prefix && i > 0 && <Text>{' '.repeat(12)}</Text>}
253
+ {renderMarkdownLine(l, `${line.id}-${i}`)}
254
+ </Box>
255
+ ))}
256
+ </Box>
257
+ );
258
+ }
259
+
260
+ // Single line with markdown
261
+ if (line.markdown) {
262
+ return (
263
+ <Box>
264
+ {line.indent && <Text> </Text>}
265
+ {line.prefix && (
266
+ <Text color={line.prefixColor} bold>
267
+ {(line.prefix).padEnd(12)}
268
+ </Text>
269
+ )}
270
+ {renderMarkdownLine(line.text, line.id)}
271
+ </Box>
272
+ );
273
+ }
274
+
275
+ // Plain text (tools, system messages)
235
276
  return (
236
277
  <Box>
237
278
  {line.indent && <Text> </Text>}
@@ -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
+ }