hoomanjs 1.13.0 → 1.15.0

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.
Files changed (35) hide show
  1. package/package.json +4 -1
  2. package/src/acp/utils/tool-kind.ts +15 -0
  3. package/src/chat/app.tsx +1 -1
  4. package/src/chat/components/ChatMessage.tsx +13 -10
  5. package/src/chat/components/Composer.tsx +1 -1
  6. package/src/chat/components/PromptInput.tsx +1 -1
  7. package/src/chat/components/QueuedPrompts.tsx +1 -1
  8. package/src/chat/components/markdown/BlockRenderer.tsx +207 -0
  9. package/src/chat/components/markdown/CodeBlock.tsx +100 -0
  10. package/src/chat/components/markdown/InlineRenderer.tsx +122 -0
  11. package/src/chat/components/markdown/MarkdownMessage.tsx +35 -0
  12. package/src/chat/components/markdown/MarkdownTable.tsx +23 -0
  13. package/src/chat/components/markdown/hooks/useMarkdownTableLayout.ts +242 -0
  14. package/src/chat/components/markdown/hooks/useMarkdownTokens.ts +46 -0
  15. package/src/chat/components/markdown/lexer.ts +100 -0
  16. package/src/chat/components/prompt-input/hooks/usePromptInputController.ts +4 -0
  17. package/src/chat/components/shared.ts +1 -1
  18. package/src/configure/app.tsx +17 -0
  19. package/src/core/agent/index.ts +38 -15
  20. package/src/core/agents/definitions.ts +47 -0
  21. package/src/core/agents/index.ts +15 -0
  22. package/src/core/agents/registry.ts +108 -0
  23. package/src/core/agents/runner.ts +375 -0
  24. package/src/core/agents/tools.ts +100 -0
  25. package/src/core/approvals/allowed-tools.ts +29 -4
  26. package/src/core/config.ts +23 -0
  27. package/src/core/prompts/agents/plan.md +35 -0
  28. package/src/core/prompts/agents/research.md +32 -0
  29. package/src/core/prompts/environment.ts +62 -0
  30. package/src/core/prompts/static/environment.md +15 -0
  31. package/src/core/prompts/static/sleep.md +20 -0
  32. package/src/core/prompts/static/subagents.md +28 -0
  33. package/src/core/prompts/system.ts +9 -0
  34. package/src/core/tools/index.ts +1 -0
  35. package/src/core/tools/sleep.ts +51 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hoomanjs",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Hackable Bun-powered AI agent toolkit for building local CLI, ACP, MCP, and channel-driven workflows.",
5
5
  "author": {
6
6
  "name": "Vaibhav Pandey",
@@ -61,17 +61,20 @@
61
61
  "@mozilla/readability": "^0.6.0",
62
62
  "@strands-agents/sdk": "^1.0.0-rc.3",
63
63
  "chromadb": "^3.4.3",
64
+ "cli-highlight": "^2.1.11",
64
65
  "cli-spinners": "^3.4.0",
65
66
  "commander": "^14.0.3",
66
67
  "fastq": "^1.20.1",
67
68
  "gray-matter": "^4.0.3",
68
69
  "handlebars": "^4.7.9",
69
70
  "ink": "^7.0.0",
71
+ "ink-ansi": "^1.0.0",
70
72
  "ink-select-input": "^6.2.0",
71
73
  "ink-text-input": "^6.0.0",
72
74
  "jsdom": "^29.0.2",
73
75
  "lodash": "^4.18.1",
74
76
  "luxon": "^3.7.2",
77
+ "marked": "^18.0.2",
75
78
  "ollama": "^0.6.3",
76
79
  "openai": "^6.34.0",
77
80
  "react": "^19.2.5",
@@ -14,6 +14,7 @@ const KNOWN_TOOL_KINDS = new Map<string, ToolKind>([
14
14
  ["search_files", "search"],
15
15
  ["get_file_info", "read"],
16
16
  ["shell", "execute"],
17
+ ["sleep", "other"],
17
18
  ["fetch", "fetch"],
18
19
  ["wiki_list_files", "read"],
19
20
  ["wiki_read_file", "read"],
@@ -22,9 +23,23 @@ const KNOWN_TOOL_KINDS = new Map<string, ToolKind>([
22
23
  ["wiki_stats", "read"],
23
24
  ["wiki_search", "search"],
24
25
  ["think", "think"],
26
+ ["run_agents", "other"],
25
27
  ["update_todos", "other"],
26
28
  ["get_current_time", "other"],
27
29
  ["convert_time", "other"],
30
+ ["list_skills", "read"],
31
+ ["search_skills", "search"],
32
+ ["install_skill", "edit"],
33
+ ["delete_skill", "edit"],
34
+ ["store_memory", "edit"],
35
+ ["search_memory", "search"],
36
+ ["update_memory", "edit"],
37
+ ["archive_memory", "edit"],
38
+ ["list_mcp_servers", "read"],
39
+ ["get_mcp_server", "read"],
40
+ ["add_mcp_server", "edit"],
41
+ ["update_mcp_server", "edit"],
42
+ ["delete_mcp_server", "edit"],
28
43
  ]);
29
44
 
30
45
  export { INTERNAL_ALWAYS_ALLOWED };
package/src/chat/app.tsx CHANGED
@@ -30,7 +30,7 @@ import { Transcript } from "./components/Transcript.tsx";
30
30
  import type { ApprovalRequest, ChatLine } from "./types.ts";
31
31
  import { getTodoViewState, type TodoViewState } from "../core/tools/todo.ts";
32
32
  import { attachmentPathsToPromptBlocks } from "../core/utils/attachments.ts";
33
- import type { PromptSubmission } from "./components/prompt-input/usePromptInputController.ts";
33
+ import type { PromptSubmission } from "./components/prompt-input/hooks/usePromptInputController.ts";
34
34
 
35
35
  type ChatAppProps = {
36
36
  agent: Agent;
@@ -1,6 +1,7 @@
1
1
  import { Box, Text } from "ink";
2
2
  import type { ChatLine } from "../types.ts";
3
3
  import { lineColor } from "./shared.ts";
4
+ import { MarkdownMessage } from "./markdown/MarkdownMessage.tsx";
4
5
  import { ReasoningStrip } from "./ReasoningStrip.tsx";
5
6
  import { ThinkingStatus } from "./ThinkingStatus.tsx";
6
7
 
@@ -17,7 +18,9 @@ export function ChatMessage({ line, liveReasoning = "" }: ChatMessageProps) {
17
18
  ? "Assistant"
18
19
  : (line.title ?? "System");
19
20
  const isPendingAssistant = line.role === "assistant" && !line.done;
20
- const text = line.content.trim() || (line.done ? "(empty)" : "");
21
+ const rawText =
22
+ line.role === "assistant" ? line.content : line.content.trim();
23
+ const text = rawText || (line.done ? "(empty)" : "");
21
24
  const shouldShowBody = Boolean(text) || !isPendingAssistant;
22
25
 
23
26
  return (
@@ -32,15 +35,15 @@ export function ChatMessage({ line, liveReasoning = "" }: ChatMessageProps) {
32
35
  <ReasoningStrip text={liveReasoning} maxVisibleLines={2} />
33
36
  ) : null}
34
37
  {shouldShowBody ? (
35
- <Text
36
- color={
37
- line.role === "user" || line.role === "assistant"
38
- ? undefined
39
- : lineColor(line)
40
- }
41
- >
42
- {text}
43
- </Text>
38
+ line.role === "assistant" ? (
39
+ <MarkdownMessage streaming={isPendingAssistant}>
40
+ {text}
41
+ </MarkdownMessage>
42
+ ) : (
43
+ <Text color={line.role === "user" ? undefined : lineColor(line)}>
44
+ {text}
45
+ </Text>
46
+ )
44
47
  ) : null}
45
48
  </Box>
46
49
  );
@@ -1,6 +1,6 @@
1
1
  import { Box, Text } from "ink";
2
2
  import { PromptInput } from "./PromptInput.tsx";
3
- import type { PromptSubmission } from "./prompt-input/usePromptInputController.ts";
3
+ import type { PromptSubmission } from "./prompt-input/hooks/usePromptInputController.ts";
4
4
 
5
5
  type ComposerProps = {
6
6
  input: string;
@@ -4,7 +4,7 @@ import { splitLineAtCursor } from "./prompt-input/render.ts";
4
4
  import {
5
5
  usePromptInputController,
6
6
  type PromptSubmission,
7
- } from "./prompt-input/usePromptInputController.ts";
7
+ } from "./prompt-input/hooks/usePromptInputController.ts";
8
8
 
9
9
  export type PromptInputProps = {
10
10
  value: string;
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Box, Text, useStdout } from "ink";
3
- import type { PromptSubmission } from "./prompt-input/usePromptInputController.ts";
3
+ import type { PromptSubmission } from "./prompt-input/hooks/usePromptInputController.ts";
4
4
 
5
5
  type QueuedPromptsProps = {
6
6
  prompts: readonly { id: string; prompt: PromptSubmission }[];
@@ -0,0 +1,207 @@
1
+ import React from "react";
2
+ import { Box, Text, useWindowSize } from "ink";
3
+ import type { Token, Tokens } from "marked";
4
+ import { CodeBlock } from "./CodeBlock.tsx";
5
+ import { inlineToPlainText, renderInlineTokens } from "./InlineRenderer.tsx";
6
+ import { MarkdownTable } from "./MarkdownTable.tsx";
7
+
8
+ type BlockRendererProps = {
9
+ tokens: Token[];
10
+ streaming?: boolean;
11
+ depth?: number;
12
+ };
13
+
14
+ function blockToPlainText(token: Token): string {
15
+ switch (token.type) {
16
+ case "paragraph":
17
+ return inlineToPlainText(token.tokens);
18
+ case "text":
19
+ return token.text ?? "";
20
+ case "code":
21
+ return token.text ?? "";
22
+ case "heading":
23
+ return inlineToPlainText(token.tokens);
24
+ case "blockquote":
25
+ return (token.tokens ?? [])
26
+ .map((child) => blockToPlainText(child))
27
+ .join("\n");
28
+ case "list":
29
+ return token.items
30
+ .map((item: Tokens.ListItem) =>
31
+ (item.tokens ?? [])
32
+ .map((child: Token) => blockToPlainText(child))
33
+ .join(" "),
34
+ )
35
+ .join("\n");
36
+ default:
37
+ return token.raw ?? "";
38
+ }
39
+ }
40
+
41
+ function renderBlock(
42
+ token: Token,
43
+ key: string,
44
+ depth: number,
45
+ columns: number,
46
+ streaming: boolean,
47
+ ): React.ReactNode | null {
48
+ switch (token.type) {
49
+ case "paragraph":
50
+ return (
51
+ <Box key={key} marginBottom={1}>
52
+ <Text wrap="wrap">
53
+ {renderInlineTokens(token.tokens, { keyPrefix: key })}
54
+ </Text>
55
+ </Box>
56
+ );
57
+ case "heading": {
58
+ const headingColor = token.depth <= 2 ? "cyan" : "white";
59
+ return (
60
+ <Box key={key} marginBottom={1}>
61
+ <Text bold color={headingColor} wrap="wrap">
62
+ {renderInlineTokens(token.tokens, { keyPrefix: key })}
63
+ </Text>
64
+ </Box>
65
+ );
66
+ }
67
+ case "code":
68
+ return (
69
+ <CodeBlock
70
+ key={key}
71
+ code={token.text ?? ""}
72
+ language={token.lang}
73
+ streaming={streaming}
74
+ />
75
+ );
76
+ case "blockquote": {
77
+ const lines = (token.tokens ?? [])
78
+ .map((child) => blockToPlainText(child))
79
+ .join("\n")
80
+ .split("\n");
81
+ return (
82
+ <Box key={key} flexDirection="column" marginBottom={1}>
83
+ {lines.map((line, index) => (
84
+ <Text key={`${key}-${index}`} color="gray" italic>
85
+ {`│ ${line || " "}`}
86
+ </Text>
87
+ ))}
88
+ </Box>
89
+ );
90
+ }
91
+ case "list":
92
+ return (
93
+ <Box key={key} flexDirection="column" marginBottom={1}>
94
+ {token.items.map((item: Tokens.ListItem, index: number) => {
95
+ const marker = token.ordered
96
+ ? `${(token.start ?? 1) + index}.`
97
+ : "-";
98
+ const checkbox =
99
+ item.task === true ? (item.checked ? "[x] " : "[ ] ") : "";
100
+ const itemTokens = item.tokens ?? [];
101
+ const [head, ...tail] = itemTokens as Token[];
102
+ const isInlineHead =
103
+ head?.type === "text" || head?.type === "paragraph";
104
+ const headNode =
105
+ head && isInlineHead ? (
106
+ <Text wrap="wrap">
107
+ {head.type === "paragraph"
108
+ ? renderInlineTokens(head.tokens, {
109
+ keyPrefix: `${key}-item-${index}-paragraph`,
110
+ })
111
+ : (head.text ??
112
+ renderInlineTokens(head.tokens, {
113
+ keyPrefix: `${key}-item-${index}-text`,
114
+ }))}
115
+ </Text>
116
+ ) : head ? (
117
+ renderBlock(
118
+ head,
119
+ `${key}-item-${index}-head`,
120
+ depth + 1,
121
+ columns,
122
+ streaming,
123
+ )
124
+ ) : (
125
+ <Text> </Text>
126
+ );
127
+ return (
128
+ <Box key={`${key}-item-${index}`} flexDirection="column">
129
+ <Box flexDirection="row">
130
+ <Text>{`${" ".repeat(depth)}${marker} ${checkbox}`}</Text>
131
+ <Box flexDirection="column" flexGrow={1}>
132
+ {headNode}
133
+ </Box>
134
+ </Box>
135
+ {tail.length > 0 ? (
136
+ <Box
137
+ marginLeft={Math.max(2, depth * 2 + 4)}
138
+ flexDirection="column"
139
+ >
140
+ {tail.map((nestedToken: Token, nestedIndex: number) =>
141
+ renderBlock(
142
+ nestedToken,
143
+ `${key}-item-${index}-tail-${nestedIndex}`,
144
+ depth + 1,
145
+ columns,
146
+ streaming,
147
+ ),
148
+ )}
149
+ </Box>
150
+ ) : null}
151
+ </Box>
152
+ );
153
+ })}
154
+ </Box>
155
+ );
156
+ case "table":
157
+ return <MarkdownTable key={key} token={token as Tokens.Table} />;
158
+ case "hr":
159
+ return (
160
+ <Box key={key} marginBottom={1}>
161
+ <Text color="gray">
162
+ {"─".repeat(Math.max(8, Math.min(columns - 4, 48)))}
163
+ </Text>
164
+ </Box>
165
+ );
166
+ case "space":
167
+ return null;
168
+ case "text":
169
+ case "escape":
170
+ return (
171
+ <Box key={key} marginBottom={1}>
172
+ <Text wrap="wrap">{token.text ?? token.raw ?? ""}</Text>
173
+ </Box>
174
+ );
175
+ case "html":
176
+ case "def":
177
+ case "del":
178
+ return null;
179
+ default:
180
+ return (
181
+ <Box key={key} marginBottom={1}>
182
+ <Text wrap="wrap">{token.raw ?? ""}</Text>
183
+ </Box>
184
+ );
185
+ }
186
+ }
187
+
188
+ export function BlockRenderer({
189
+ tokens,
190
+ streaming = false,
191
+ depth = 0,
192
+ }: BlockRendererProps) {
193
+ const { columns } = useWindowSize();
194
+ return (
195
+ <Box flexDirection="column">
196
+ {tokens.map((token, index) =>
197
+ renderBlock(
198
+ token,
199
+ `block-${depth}-${index}`,
200
+ depth,
201
+ columns,
202
+ streaming,
203
+ ),
204
+ )}
205
+ </Box>
206
+ );
207
+ }
@@ -0,0 +1,100 @@
1
+ import { createRequire } from "node:module";
2
+ import type React from "react";
3
+ import { useMemo } from "react";
4
+ import { Box, Text } from "ink";
5
+ import { highlight, supportsLanguage } from "cli-highlight";
6
+
7
+ type CodeBlockProps = {
8
+ code: string;
9
+ language?: string;
10
+ streaming?: boolean;
11
+ };
12
+
13
+ type InkAnsiComponent = React.ComponentType<{
14
+ children?: React.ReactNode;
15
+ }>;
16
+
17
+ const localRequire = createRequire(import.meta.url);
18
+ const ANSI_RE = /\u001b\[[0-9;]*m/g;
19
+ let cachedInkAnsi: InkAnsiComponent | null | undefined;
20
+
21
+ function stripAnsi(value: string): string {
22
+ return value.replace(ANSI_RE, "");
23
+ }
24
+
25
+ function getInkAnsi(): InkAnsiComponent | null {
26
+ if (cachedInkAnsi !== undefined) {
27
+ return cachedInkAnsi;
28
+ }
29
+ try {
30
+ const moduleValue = localRequire("ink-ansi") as
31
+ | InkAnsiComponent
32
+ | { default?: InkAnsiComponent };
33
+ cachedInkAnsi =
34
+ typeof moduleValue === "function"
35
+ ? moduleValue
36
+ : (moduleValue.default ?? null);
37
+ } catch {
38
+ cachedInkAnsi = null;
39
+ }
40
+ return cachedInkAnsi;
41
+ }
42
+
43
+ function renderCodeLines(lines: string[], color?: string) {
44
+ return lines.map((line, index) => (
45
+ <Text key={index} color={color}>
46
+ {line || " "}
47
+ </Text>
48
+ ));
49
+ }
50
+
51
+ export function CodeBlock({
52
+ code,
53
+ language,
54
+ streaming = false,
55
+ }: CodeBlockProps) {
56
+ const trimmedLanguage = language?.trim() || undefined;
57
+ const normalizedCode = useMemo(() => code.replace(/\r\n/g, "\n"), [code]);
58
+ const plainLines = useMemo(
59
+ () => normalizedCode.split("\n"),
60
+ [normalizedCode],
61
+ );
62
+ const highlightedLines = useMemo(() => {
63
+ if (streaming || !trimmedLanguage || !supportsLanguage(trimmedLanguage)) {
64
+ return null;
65
+ }
66
+ try {
67
+ return highlight(normalizedCode, {
68
+ language: trimmedLanguage,
69
+ ignoreIllegals: true,
70
+ }).split("\n");
71
+ } catch {
72
+ return null;
73
+ }
74
+ }, [normalizedCode, streaming, trimmedLanguage]);
75
+
76
+ if (highlightedLines) {
77
+ const InkAnsiText = getInkAnsi();
78
+ return (
79
+ <Box flexDirection="column" marginBottom={1}>
80
+ <Text color="gray">{`\`\`\`${trimmedLanguage}`}</Text>
81
+ {highlightedLines.map((line, index) =>
82
+ InkAnsiText ? (
83
+ <InkAnsiText key={index}>{line || " "}</InkAnsiText>
84
+ ) : (
85
+ <Text key={index}>{stripAnsi(line) || " "}</Text>
86
+ ),
87
+ )}
88
+ <Text color="gray">```</Text>
89
+ </Box>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <Box flexDirection="column" marginBottom={1}>
95
+ <Text color="gray">{`\`\`\`${trimmedLanguage ?? ""}`}</Text>
96
+ {renderCodeLines(plainLines, "white")}
97
+ <Text color="gray">```</Text>
98
+ </Box>
99
+ );
100
+ }
@@ -0,0 +1,122 @@
1
+ import React from "react";
2
+ import { Text } from "ink";
3
+ import type { Token } from "marked";
4
+
5
+ function plainFromToken(token: Token): string {
6
+ switch (token.type) {
7
+ case "text":
8
+ return token.text ?? "";
9
+ case "codespan":
10
+ return token.text ?? "";
11
+ case "escape":
12
+ return token.text ?? "";
13
+ case "link": {
14
+ const label = inlineToPlainText(token.tokens);
15
+ return label || token.href || "";
16
+ }
17
+ case "strong":
18
+ case "em":
19
+ case "del":
20
+ return inlineToPlainText(token.tokens);
21
+ case "image":
22
+ return token.text || token.href || "";
23
+ case "br":
24
+ return "\n";
25
+ default:
26
+ return token.raw ?? "";
27
+ }
28
+ }
29
+
30
+ export function inlineToPlainText(tokens: Token[] | undefined): string {
31
+ if (!tokens || tokens.length === 0) {
32
+ return "";
33
+ }
34
+ return tokens.map((token) => plainFromToken(token)).join("");
35
+ }
36
+
37
+ type RenderInlineOptions = {
38
+ keyPrefix?: string;
39
+ };
40
+
41
+ export function renderInlineTokens(
42
+ tokens: Token[] | undefined,
43
+ options: RenderInlineOptions = {},
44
+ ): React.ReactNode[] {
45
+ if (!tokens || tokens.length === 0) {
46
+ return [];
47
+ }
48
+ const prefix = options.keyPrefix ?? "inline";
49
+ return tokens.flatMap((token, index) => {
50
+ const key = `${prefix}-${index}`;
51
+ switch (token.type) {
52
+ case "text":
53
+ return [token.text ?? ""];
54
+ case "escape":
55
+ return [token.text ?? token.raw ?? ""];
56
+ case "codespan":
57
+ return [
58
+ <Text key={key} color="green">
59
+ {token.text ?? ""}
60
+ </Text>,
61
+ ];
62
+ case "strong":
63
+ return [
64
+ <Text key={key} bold>
65
+ {renderInlineTokens(token.tokens, { keyPrefix: key })}
66
+ </Text>,
67
+ ];
68
+ case "em":
69
+ return [
70
+ <Text key={key} italic>
71
+ {renderInlineTokens(token.tokens, { keyPrefix: key })}
72
+ </Text>,
73
+ ];
74
+ case "del":
75
+ return [
76
+ <Text key={key} dimColor>
77
+ {renderInlineTokens(token.tokens, { keyPrefix: key })}
78
+ </Text>,
79
+ ];
80
+ case "link": {
81
+ const href = token.href ?? "";
82
+ const text = inlineToPlainText(token.tokens).trim();
83
+ const label = text || href;
84
+ const shouldShowHref = Boolean(href) && label !== href;
85
+ return [
86
+ <Text key={`${key}-label`} color="blue" underline>
87
+ {label}
88
+ </Text>,
89
+ ...(shouldShowHref
90
+ ? [
91
+ <Text key={`${key}-href`} color="gray">
92
+ {` (${href})`}
93
+ </Text>,
94
+ ]
95
+ : []),
96
+ ];
97
+ }
98
+ case "image": {
99
+ const label = token.text?.trim() || "image";
100
+ const href = token.href ?? "";
101
+ return [
102
+ <Text key={`${key}-img-label`} color="magenta">
103
+ {`[${label}]`}
104
+ </Text>,
105
+ ...(href
106
+ ? [
107
+ <Text key={`${key}-img-href`} color="gray">
108
+ {` (${href})`}
109
+ </Text>,
110
+ ]
111
+ : []),
112
+ ];
113
+ }
114
+ case "br":
115
+ return [<Text key={key}>{"\n"}</Text>];
116
+ default: {
117
+ const fallback = plainFromToken(token);
118
+ return fallback ? [fallback] : [];
119
+ }
120
+ }
121
+ });
122
+ }
@@ -0,0 +1,35 @@
1
+ import { Box } from "ink";
2
+ import { BlockRenderer } from "./BlockRenderer.tsx";
3
+ import { useMarkdownTokens } from "./hooks/useMarkdownTokens.ts";
4
+
5
+ type MarkdownMessageProps = {
6
+ children: string;
7
+ streaming?: boolean;
8
+ };
9
+
10
+ export function MarkdownMessage({
11
+ children,
12
+ streaming = false,
13
+ }: MarkdownMessageProps) {
14
+ const content = children ?? "";
15
+ const {
16
+ fullTokens,
17
+ stablePrefix,
18
+ unstableSuffix,
19
+ stableTokens,
20
+ unstableTokens,
21
+ } = useMarkdownTokens(content, streaming);
22
+
23
+ if (!streaming) {
24
+ return <BlockRenderer tokens={fullTokens} />;
25
+ }
26
+
27
+ return (
28
+ <Box flexDirection="column">
29
+ {stablePrefix ? <BlockRenderer tokens={stableTokens} /> : null}
30
+ {unstableSuffix ? (
31
+ <BlockRenderer tokens={unstableTokens} streaming />
32
+ ) : null}
33
+ </Box>
34
+ );
35
+ }
@@ -0,0 +1,23 @@
1
+ import { Box, Text, useWindowSize } from "ink";
2
+ import type { Tokens } from "marked";
3
+ import { useMarkdownTableLayout } from "./hooks/useMarkdownTableLayout.ts";
4
+
5
+ type MarkdownTableProps = {
6
+ token: Tokens.Table;
7
+ };
8
+
9
+ export function MarkdownTable({ token }: MarkdownTableProps) {
10
+ const { columns } = useWindowSize();
11
+ const layout = useMarkdownTableLayout(token, columns);
12
+ if (layout.lines.length === 0) {
13
+ return null;
14
+ }
15
+
16
+ return (
17
+ <Box flexDirection="column" marginBottom={1}>
18
+ {layout.lines.map((line, index) => (
19
+ <Text key={index}>{line}</Text>
20
+ ))}
21
+ </Box>
22
+ );
23
+ }