wave-code 0.5.0 → 0.6.1

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 (95) hide show
  1. package/dist/components/App.d.ts.map +1 -1
  2. package/dist/components/App.js +38 -2
  3. package/dist/components/BackgroundTaskManager.d.ts +6 -0
  4. package/dist/components/BackgroundTaskManager.d.ts.map +1 -0
  5. package/dist/components/{TaskManager.js → BackgroundTaskManager.js} +1 -1
  6. package/dist/components/ChatInterface.d.ts.map +1 -1
  7. package/dist/components/ChatInterface.js +39 -5
  8. package/dist/components/CommandSelector.d.ts.map +1 -1
  9. package/dist/components/CommandSelector.js +10 -2
  10. package/dist/components/CompressDisplay.d.ts.map +1 -1
  11. package/dist/components/CompressDisplay.js +6 -10
  12. package/dist/components/ConfirmationDetails.d.ts +9 -0
  13. package/dist/components/ConfirmationDetails.d.ts.map +1 -0
  14. package/dist/components/ConfirmationDetails.js +53 -0
  15. package/dist/components/{Confirmation.d.ts → ConfirmationSelector.d.ts} +3 -3
  16. package/dist/components/ConfirmationSelector.d.ts.map +1 -0
  17. package/dist/components/{Confirmation.js → ConfirmationSelector.js} +34 -96
  18. package/dist/components/DiffDisplay.d.ts.map +1 -1
  19. package/dist/components/DiffDisplay.js +44 -1
  20. package/dist/components/FileSelector.d.ts.map +1 -1
  21. package/dist/components/FileSelector.js +2 -2
  22. package/dist/components/HistorySearch.d.ts.map +1 -1
  23. package/dist/components/HistorySearch.js +12 -4
  24. package/dist/components/InputBox.d.ts +1 -3
  25. package/dist/components/InputBox.d.ts.map +1 -1
  26. package/dist/components/InputBox.js +7 -17
  27. package/dist/components/LoadingIndicator.d.ts +11 -0
  28. package/dist/components/LoadingIndicator.d.ts.map +1 -0
  29. package/dist/components/LoadingIndicator.js +6 -0
  30. package/dist/components/Markdown.d.ts.map +1 -1
  31. package/dist/components/Markdown.js +114 -121
  32. package/dist/components/MessageItem.d.ts.map +1 -1
  33. package/dist/components/MessageItem.js +1 -2
  34. package/dist/components/MessageList.d.ts +2 -3
  35. package/dist/components/MessageList.d.ts.map +1 -1
  36. package/dist/components/MessageList.js +7 -7
  37. package/dist/components/PlanDisplay.d.ts.map +1 -1
  38. package/dist/components/PlanDisplay.js +4 -12
  39. package/dist/components/RewindCommand.d.ts +4 -0
  40. package/dist/components/RewindCommand.d.ts.map +1 -1
  41. package/dist/components/RewindCommand.js +19 -2
  42. package/dist/components/SubagentBlock.d.ts.map +1 -1
  43. package/dist/components/SubagentBlock.js +9 -6
  44. package/dist/components/TaskList.d.ts +3 -0
  45. package/dist/components/TaskList.d.ts.map +1 -0
  46. package/dist/components/TaskList.js +49 -0
  47. package/dist/components/ToolResultDisplay.js +1 -1
  48. package/dist/contexts/useChat.d.ts +11 -3
  49. package/dist/contexts/useChat.d.ts.map +1 -1
  50. package/dist/contexts/useChat.js +36 -31
  51. package/dist/hooks/useInputManager.d.ts +2 -13
  52. package/dist/hooks/useInputManager.d.ts.map +1 -1
  53. package/dist/hooks/useInputManager.js +8 -57
  54. package/dist/hooks/useTasks.d.ts +2 -0
  55. package/dist/hooks/useTasks.d.ts.map +1 -0
  56. package/dist/hooks/useTasks.js +5 -0
  57. package/dist/managers/InputManager.d.ts +4 -28
  58. package/dist/managers/InputManager.d.ts.map +1 -1
  59. package/dist/managers/InputManager.js +22 -128
  60. package/package.json +5 -6
  61. package/src/components/App.tsx +50 -3
  62. package/src/components/{TaskManager.tsx → BackgroundTaskManager.tsx} +4 -2
  63. package/src/components/ChatInterface.tsx +79 -23
  64. package/src/components/CommandSelector.tsx +35 -17
  65. package/src/components/CompressDisplay.tsx +5 -22
  66. package/src/components/ConfirmationDetails.tsx +108 -0
  67. package/src/components/{Confirmation.tsx → ConfirmationSelector.tsx} +69 -184
  68. package/src/components/DiffDisplay.tsx +62 -1
  69. package/src/components/FileSelector.tsx +0 -2
  70. package/src/components/HistorySearch.tsx +45 -21
  71. package/src/components/InputBox.tsx +11 -33
  72. package/src/components/LoadingIndicator.tsx +56 -0
  73. package/src/components/Markdown.tsx +126 -323
  74. package/src/components/MessageItem.tsx +1 -3
  75. package/src/components/MessageList.tsx +10 -67
  76. package/src/components/PlanDisplay.tsx +4 -27
  77. package/src/components/RewindCommand.tsx +38 -1
  78. package/src/components/SubagentBlock.tsx +25 -16
  79. package/src/components/TaskList.tsx +70 -0
  80. package/src/components/ToolResultDisplay.tsx +2 -2
  81. package/src/contexts/useChat.tsx +57 -40
  82. package/src/hooks/useInputManager.ts +9 -73
  83. package/src/hooks/useTasks.ts +6 -0
  84. package/src/managers/InputManager.ts +25 -159
  85. package/dist/components/Confirmation.d.ts.map +0 -1
  86. package/dist/components/MemoryDisplay.d.ts +0 -8
  87. package/dist/components/MemoryDisplay.d.ts.map +0 -1
  88. package/dist/components/MemoryDisplay.js +0 -25
  89. package/dist/components/MemoryTypeSelector.d.ts +0 -8
  90. package/dist/components/MemoryTypeSelector.d.ts.map +0 -1
  91. package/dist/components/MemoryTypeSelector.js +0 -38
  92. package/dist/components/TaskManager.d.ts +0 -6
  93. package/dist/components/TaskManager.d.ts.map +0 -1
  94. package/src/components/MemoryDisplay.tsx +0 -62
  95. package/src/components/MemoryTypeSelector.tsx +0 -98
@@ -4,8 +4,7 @@ import { useInput } from "ink";
4
4
  import { FileSelector } from "./FileSelector.js";
5
5
  import { CommandSelector } from "./CommandSelector.js";
6
6
  import { HistorySearch } from "./HistorySearch.js";
7
- import { MemoryTypeSelector } from "./MemoryTypeSelector.js";
8
- import { TaskManager } from "./TaskManager.js";
7
+ import { BackgroundTaskManager } from "./BackgroundTaskManager.js";
9
8
  import { McpManager } from "./McpManager.js";
10
9
  import { RewindCommand } from "./RewindCommand.js";
11
10
  import { useInputManager } from "../hooks/useInputManager.js";
@@ -14,7 +13,7 @@ import { useChat } from "../contexts/useChat.js";
14
13
  import type { McpServerStatus, SlashCommand } from "wave-agent-sdk";
15
14
 
16
15
  export const INPUT_PLACEHOLDER_TEXT =
17
- "Type your message (use @ to reference files, / for commands, # to add memory, Ctrl+R to search history)...";
16
+ "Type your message (use @ to reference files, / for commands, Ctrl+R to search history, Ctrl+O to expand messages, Ctrl+T to toggle tasks)...";
18
17
 
19
18
  export const INPUT_PLACEHOLDER_TEXT_PREFIX = INPUT_PLACEHOLDER_TEXT.substring(
20
19
  0,
@@ -25,13 +24,11 @@ export interface InputBoxProps {
25
24
  isLoading?: boolean;
26
25
  isCommandRunning?: boolean;
27
26
  workdir?: string;
28
- userInputHistory?: string[];
29
27
  sendMessage?: (
30
28
  message: string,
31
29
  images?: Array<{ path: string; mimeType: string }>,
32
30
  ) => void;
33
31
  abortMessage?: () => void;
34
- saveMemory?: (message: string, type: "project" | "user") => Promise<void>;
35
32
  // MCP related properties
36
33
  mcpServers?: McpServerStatus[];
37
34
  connectMcpServer?: (serverName: string) => Promise<boolean>;
@@ -44,10 +41,8 @@ export interface InputBoxProps {
44
41
  export const InputBox: React.FC<InputBoxProps> = ({
45
42
  isLoading = false,
46
43
  isCommandRunning = false,
47
- userInputHistory = [],
48
44
  sendMessage = () => {},
49
45
  abortMessage = () => {},
50
- saveMemory = async () => {},
51
46
  mcpServers = [],
52
47
  connectMcpServer = async () => false,
53
48
  disconnectMcpServer = async () => false,
@@ -60,6 +55,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
60
55
  handleRewindSelect,
61
56
  backgroundCurrentTask,
62
57
  messages,
58
+ getFullMessageThread,
63
59
  } = useChat();
64
60
 
65
61
  // Input manager with all input state and functionality (including images)
@@ -83,26 +79,19 @@ export const InputBox: React.FC<InputBoxProps> = ({
83
79
  handleCancelCommandSelect,
84
80
  handleHistorySearchSelect,
85
81
  handleCancelHistorySearch,
86
- // Memory type selector
87
- showMemoryTypeSelector,
88
- memoryMessage,
89
- handleMemoryTypeSelect,
90
- handleCancelMemoryTypeSelect,
91
82
  // History search
92
83
  showHistorySearch,
93
84
  historySearchQuery,
94
85
  // Task/MCP Manager
95
- showTaskManager,
86
+ showBackgroundTaskManager,
96
87
  showMcpManager,
97
88
  showRewindManager,
98
- setShowTaskManager,
89
+ setShowBackgroundTaskManager,
99
90
  setShowMcpManager,
100
91
  setShowRewindManager,
101
92
  // Permission mode
102
93
  permissionMode,
103
94
  setPermissionMode,
104
- // Input history
105
- setUserInputHistory,
106
95
  // Main handler
107
96
  handleInput,
108
97
  // Manager ready state
@@ -110,7 +99,6 @@ export const InputBox: React.FC<InputBoxProps> = ({
110
99
  } = useInputManager({
111
100
  onSendMessage: sendMessage,
112
101
  onHasSlashCommand: hasSlashCommand,
113
- onSaveMemory: saveMemory,
114
102
  onAbortMessage: abortMessage,
115
103
  onBackgroundCurrentTask: backgroundCurrentTask,
116
104
  onPermissionModeChange: setChatPermissionMode,
@@ -121,11 +109,6 @@ export const InputBox: React.FC<InputBoxProps> = ({
121
109
  setPermissionMode(chatPermissionMode);
122
110
  }, [chatPermissionMode, setPermissionMode]);
123
111
 
124
- // Set user input history when it changes
125
- useEffect(() => {
126
- setUserInputHistory(userInputHistory);
127
- }, [userInputHistory, setUserInputHistory]);
128
-
129
112
  // Use the InputManager's unified input handler
130
113
  useInput(async (input, key) => {
131
114
  await handleInput(
@@ -177,6 +160,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
177
160
  messages={messages}
178
161
  onSelect={handleRewindSelectWithClose}
179
162
  onCancel={handleRewindCancel}
163
+ getFullMessageThread={getFullMessageThread}
180
164
  />
181
165
  );
182
166
  }
@@ -210,18 +194,12 @@ export const InputBox: React.FC<InputBoxProps> = ({
210
194
  />
211
195
  )}
212
196
 
213
- {showMemoryTypeSelector && (
214
- <MemoryTypeSelector
215
- message={memoryMessage}
216
- onSelect={handleMemoryTypeSelect}
217
- onCancel={handleCancelMemoryTypeSelect}
197
+ {showBackgroundTaskManager && (
198
+ <BackgroundTaskManager
199
+ onCancel={() => setShowBackgroundTaskManager(false)}
218
200
  />
219
201
  )}
220
202
 
221
- {showTaskManager && (
222
- <TaskManager onCancel={() => setShowTaskManager(false)} />
223
- )}
224
-
225
203
  {showMcpManager && (
226
204
  <McpManager
227
205
  onCancel={() => setShowMcpManager(false)}
@@ -231,7 +209,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
231
209
  />
232
210
  )}
233
211
 
234
- {showTaskManager || showMcpManager || showRewindManager || (
212
+ {showBackgroundTaskManager || showMcpManager || showRewindManager || (
235
213
  <Box flexDirection="column">
236
214
  <Box
237
215
  borderStyle="single"
@@ -253,7 +231,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
253
231
  )}
254
232
  </Text>
255
233
  </Box>
256
- <Box paddingRight={1}>
234
+ <Box paddingRight={1} justifyContent="space-between" width="100%">
257
235
  <Text color="gray">
258
236
  Mode:{" "}
259
237
  <Text color={permissionMode === "plan" ? "yellow" : "cyan"}>
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ export interface LoadingIndicatorProps {
5
+ isLoading?: boolean;
6
+ isCommandRunning?: boolean;
7
+ isCompressing?: boolean;
8
+ latestTotalTokens?: number;
9
+ }
10
+
11
+ export const LoadingIndicator = ({
12
+ isLoading = false,
13
+ isCommandRunning = false,
14
+ isCompressing = false,
15
+ latestTotalTokens = 0,
16
+ }: LoadingIndicatorProps) => {
17
+ return (
18
+ <Box flexDirection="column">
19
+ {isLoading && (
20
+ <Box>
21
+ <Text color="yellow">💭 AI is thinking... </Text>
22
+ {latestTotalTokens > 0 && (
23
+ <>
24
+ <Text color="gray" dimColor>
25
+ |{" "}
26
+ </Text>
27
+ <Text color="blue" bold>
28
+ {latestTotalTokens.toLocaleString()}
29
+ </Text>
30
+ <Text color="gray" dimColor>
31
+ {" "}
32
+ tokens{" "}
33
+ </Text>
34
+ </>
35
+ )}
36
+ <Text color="gray" dimColor>
37
+ |{" "}
38
+ </Text>
39
+ <Text color="red" bold>
40
+ Esc
41
+ </Text>
42
+ <Text color="gray" dimColor>
43
+ {" "}
44
+ to abort
45
+ </Text>
46
+ </Box>
47
+ )}
48
+ {isCommandRunning && <Text color="blue">🚀 Command is running...</Text>}
49
+ {isCompressing && (
50
+ <Text color="magenta">🗜️ Compressing message history...</Text>
51
+ )}
52
+ </Box>
53
+ );
54
+ };
55
+
56
+ LoadingIndicator.displayName = "LoadingIndicator";
@@ -1,7 +1,7 @@
1
1
  import React, { useMemo } from "react";
2
- import { Box, Text, useStdout } from "ink";
3
- import { marked, type Token, type Tokens } from "marked";
4
- import { highlight } from "cli-highlight";
2
+ import { Box, Text } from "ink";
3
+ import { Renderer, marked, type Tokens } from "marked";
4
+ import chalk from "chalk";
5
5
 
6
6
  export interface MarkdownProps {
7
7
  children: string;
@@ -17,339 +17,142 @@ const unescapeHtml = (html: string) => {
17
17
  .replace(/&apos;/g, "'");
18
18
  };
19
19
 
20
- const InlineRenderer = ({ tokens }: { tokens: Token[] }) => {
21
- return (
22
- <>
23
- {tokens.map((token, index) => {
24
- switch (token.type) {
25
- case "text": {
26
- const t = token as Tokens.Text;
27
- if (t.tokens) {
28
- return <InlineRenderer key={index} tokens={t.tokens} />;
29
- }
30
- return <Text key={index}>{unescapeHtml(t.text)}</Text>;
31
- }
32
- case "strong":
33
- return (
34
- <Text key={index} bold>
35
- {token.tokens ? (
36
- <InlineRenderer tokens={token.tokens} />
37
- ) : (
38
- unescapeHtml((token as Tokens.Strong).text)
39
- )}
40
- </Text>
41
- );
42
- case "em":
43
- return (
44
- <Text key={index} italic>
45
- {token.tokens ? (
46
- <InlineRenderer tokens={token.tokens} />
47
- ) : (
48
- unescapeHtml((token as Tokens.Em).text)
49
- )}
50
- </Text>
51
- );
52
- case "codespan":
53
- return (
54
- <Text key={index} color="yellow">
55
- {unescapeHtml((token as Tokens.Codespan).text)}
56
- </Text>
57
- );
58
- case "link": {
59
- const t = token as Tokens.Link;
60
- return (
61
- <Text key={index}>
62
- <Text color="blue" underline>
63
- {t.tokens ? (
64
- <InlineRenderer tokens={t.tokens} />
65
- ) : (
66
- unescapeHtml(t.text)
67
- )}
68
- </Text>
69
- <Text color="gray"> ({t.href})</Text>
70
- </Text>
71
- );
72
- }
73
- case "br":
74
- return <Text key={index}>{"\n"}</Text>;
75
- case "del":
76
- return (
77
- <Text key={index} strikethrough>
78
- {token.tokens ? (
79
- <InlineRenderer tokens={token.tokens} />
80
- ) : (
81
- unescapeHtml((token as Tokens.Del).text)
82
- )}
83
- </Text>
84
- );
85
- default:
86
- return <Text key={index}>{token.raw}</Text>;
87
- }
88
- })}
89
- </>
90
- );
91
- };
92
-
93
- const TableRenderer = ({ token }: { token: Tokens.Table }) => {
94
- const { stdout } = useStdout();
95
- const terminalWidth = (stdout?.columns || 80) - 2;
20
+ class AnsiRenderer extends Renderer<string> {
21
+ override code({ text, lang }: Tokens.Code): string {
22
+ const prefix = lang ? `\`\`\`${lang}` : "```";
23
+ const suffix = "```";
24
+ return `\n${chalk.gray(prefix)}\n${text}\n${chalk.gray(suffix)}\n`;
25
+ }
96
26
 
97
- const columnWidths = useMemo(() => {
98
- const numCols = token.header.length;
99
- const minWidth = 5;
100
- const maxColWidth = 40;
101
- const widths = token.header.map((h) =>
102
- Math.min(maxColWidth, Math.max(minWidth, h.text.length)),
27
+ override blockquote({ tokens }: Tokens.Blockquote): string {
28
+ const body = this.parser.parse(tokens);
29
+ return (
30
+ "\n" +
31
+ body
32
+ .trim()
33
+ .split("\n")
34
+ .map((line) => chalk.gray("> ") + line)
35
+ .join("\n") +
36
+ "\n"
103
37
  );
38
+ }
104
39
 
105
- token.rows.forEach((row) => {
106
- row.forEach((cell, i) => {
107
- widths[i] = Math.min(
108
- maxColWidth,
109
- Math.max(widths[i] || minWidth, cell.text.length),
110
- );
111
- });
112
- });
40
+ override heading({ tokens, depth }: Tokens.Heading): string {
41
+ const text = this.parser.parseInline(tokens);
42
+ const hashes = "#".repeat(depth);
43
+ return `\n${chalk.cyan(`${hashes} ${text}`)}\n`;
44
+ }
113
45
 
114
- const paddedWidths = widths.map((w) => w + 2);
115
- const totalWidth = paddedWidths.reduce((a, b) => a + b, 0) + numCols + 1;
46
+ override hr(): string {
47
+ return `\n${chalk.gray("─".repeat(20))}\n`;
48
+ }
116
49
 
117
- if (totalWidth <= terminalWidth) {
118
- return paddedWidths;
119
- }
50
+ override list(token: Tokens.List): string {
51
+ const body = token.items
52
+ .map((item, i) => {
53
+ const text = this.listitem(item);
54
+ const prefix = token.ordered
55
+ ? chalk.gray(`${(token.start || 1) + i}. `)
56
+ : chalk.gray("• ");
57
+ const lines = text.split("\n");
58
+ const firstLine = prefix + lines[0];
59
+ const restLines = lines
60
+ .slice(1)
61
+ .filter((line) => line.length > 0)
62
+ .map((line) => " " + line);
63
+ return [firstLine, ...restLines].join("\n") + "\n";
64
+ })
65
+ .join("");
66
+ return `\n${body}`;
67
+ }
120
68
 
121
- // If table is too wide, scale down columns proportionally
122
- const availableWidth = terminalWidth - numCols - 1;
123
- const scaleFactor = availableWidth / (totalWidth - numCols - 1);
124
- return paddedWidths.map((w) =>
125
- Math.max(minWidth, Math.floor(w * scaleFactor)),
126
- );
127
- }, [token, terminalWidth]);
69
+ override listitem(item: Tokens.ListItem): string {
70
+ return `${this.parser.parse(item.tokens).trim()}\n`;
71
+ }
128
72
 
129
- return (
130
- <Box
131
- flexDirection="column"
132
- marginBottom={1}
133
- borderStyle="single"
134
- borderColor="gray"
135
- width={columnWidths.reduce((a, b) => a + b, 0) + token.header.length + 1}
136
- >
137
- {/* Header */}
138
- <Box
139
- flexDirection="row"
140
- borderStyle="single"
141
- borderBottom
142
- borderTop={false}
143
- borderLeft={false}
144
- borderRight={false}
145
- borderColor="gray"
146
- >
147
- {token.header.map((cell, i) => (
148
- <Box
149
- key={i}
150
- width={columnWidths[i]}
151
- paddingX={1}
152
- borderStyle="single"
153
- borderLeft={i > 0}
154
- borderRight={false}
155
- borderTop={false}
156
- borderBottom={false}
157
- borderColor="gray"
158
- >
159
- <Text bold wrap="wrap">
160
- <InlineRenderer tokens={cell.tokens} />
161
- </Text>
162
- </Box>
163
- ))}
164
- </Box>
165
- {/* Rows */}
166
- {token.rows.map((row, rowIndex) => (
167
- <Box key={rowIndex} flexDirection="row">
168
- {row.map((cell, i) => (
169
- <Box
170
- key={i}
171
- width={columnWidths[i]}
172
- paddingX={1}
173
- borderStyle="single"
174
- borderLeft={i > 0}
175
- borderRight={false}
176
- borderTop={false}
177
- borderBottom={false}
178
- borderColor="gray"
179
- >
180
- <Text wrap="wrap">
181
- <InlineRenderer tokens={cell.tokens} />
182
- </Text>
183
- </Box>
184
- ))}
185
- </Box>
186
- ))}
187
- </Box>
188
- );
189
- };
73
+ override checkbox({ checked }: Tokens.Checkbox): string {
74
+ return checked ? chalk.green("[x] ") : chalk.gray("[ ] ");
75
+ }
190
76
 
191
- const BlockRenderer = ({ tokens }: { tokens: Token[] }) => {
192
- return (
193
- <>
194
- {tokens.map((token, index) => {
195
- switch (token.type) {
196
- case "heading": {
197
- const t = token as Tokens.Heading;
198
- return (
199
- <Box key={index} marginBottom={1} flexDirection="column">
200
- <Text bold color="cyan">
201
- {"#".repeat(t.depth)} <InlineRenderer tokens={t.tokens} />
202
- </Text>
203
- </Box>
204
- );
205
- }
206
- case "paragraph": {
207
- const t = token as Tokens.Paragraph;
208
- return (
209
- <Box key={index} marginBottom={1} flexDirection="row">
210
- <Text>
211
- <InlineRenderer tokens={t.tokens} />
212
- </Text>
213
- </Box>
214
- );
215
- }
216
- case "code": {
217
- const t = token as Tokens.Code;
218
- if (t.lang !== undefined) {
219
- const raw = token.raw.endsWith("\n")
220
- ? token.raw.slice(0, -1)
221
- : token.raw;
222
- const lines = raw.split("\n");
223
- const opening = lines[0];
224
- const closing = lines[lines.length - 1];
225
- const content = lines.slice(1, -1).join("\n");
226
- const highlighted = content
227
- ? highlight(unescapeHtml(content), {
228
- language: t.lang,
229
- ignoreIllegals: true,
230
- })
231
- : "";
232
- return (
233
- <Box
234
- key={index}
235
- flexDirection="column"
236
- paddingX={1}
237
- marginBottom={1}
238
- >
239
- <Text color="gray">{opening}</Text>
240
- {highlighted && <Text>{highlighted}</Text>}
241
- <Text color="gray">{closing}</Text>
242
- </Box>
243
- );
244
- }
245
- return (
246
- <Box
247
- key={index}
248
- flexDirection="column"
249
- paddingX={1}
250
- marginBottom={1}
251
- >
252
- <Text>{unescapeHtml(t.text)}</Text>
253
- </Box>
254
- );
255
- }
256
- case "list": {
257
- const t = token as Tokens.List;
258
- return (
259
- <Box
260
- key={index}
261
- flexDirection="column"
262
- marginBottom={1}
263
- paddingLeft={2}
264
- >
265
- {t.items.map((item, i) => {
266
- const start = t.start || 1;
267
- return (
268
- <Box key={i} flexDirection="row">
269
- <Text color="gray">
270
- {t.ordered ? `${start + i}. ` : "• "}
271
- </Text>
272
- <Box flexDirection="column" flexGrow={1}>
273
- {item.tokens.map((itemToken, itemIndex) => {
274
- if (
275
- itemToken.type === "text" ||
276
- itemToken.type === "paragraph"
277
- ) {
278
- const it = itemToken as
279
- | Tokens.Text
280
- | Tokens.Paragraph;
281
- return (
282
- <Box key={itemIndex} flexDirection="row">
283
- <Text>
284
- <InlineRenderer
285
- tokens={it.tokens || [itemToken]}
286
- />
287
- </Text>
288
- </Box>
289
- );
290
- }
291
- return (
292
- <BlockRenderer
293
- key={itemIndex}
294
- tokens={[itemToken]}
295
- />
296
- );
297
- })}
298
- </Box>
299
- </Box>
300
- );
301
- })}
302
- </Box>
303
- );
304
- }
305
- case "blockquote": {
306
- const t = token as Tokens.Blockquote;
307
- return (
308
- <Box
309
- key={index}
310
- flexDirection="column"
311
- paddingLeft={2}
312
- borderStyle="single"
313
- borderLeft
314
- borderRight={false}
315
- borderTop={false}
316
- borderBottom={false}
317
- borderColor="gray"
318
- marginBottom={1}
319
- >
320
- <BlockRenderer tokens={t.tokens} />
321
- </Box>
322
- );
323
- }
324
- case "hr":
325
- return (
326
- <Box key={index} marginBottom={1}>
327
- <Text color="gray">{"─".repeat(20)}</Text>
328
- </Box>
329
- );
330
- case "table":
331
- return <TableRenderer key={index} token={token as Tokens.Table} />;
332
- case "space":
333
- return null;
334
- default:
335
- return (
336
- <Box key={index} marginBottom={1}>
337
- <Text>{token.raw}</Text>
338
- </Box>
339
- );
340
- }
341
- })}
342
- </>
343
- );
344
- };
77
+ override paragraph({ tokens }: Tokens.Paragraph): string {
78
+ const text = this.parser.parseInline(tokens);
79
+ return `\n${text}\n`;
80
+ }
81
+
82
+ override table(token: Tokens.Table): string {
83
+ const header = token.header.map((cell) => this.tablecell(cell)).join("");
84
+ const body = token.rows
85
+ .map((row) => row.map((cell) => this.tablecell(cell)).join("") + "\n")
86
+ .join("");
87
+ return `\n${header}\n${body}\n`;
88
+ }
89
+
90
+ override tablerow({ text }: Tokens.TableRow): string {
91
+ return text + "\n";
92
+ }
93
+
94
+ override tablecell(token: Tokens.TableCell): string {
95
+ const text = token.header ? chalk.bold(token.text) : token.text;
96
+ return text + " | ";
97
+ }
98
+
99
+ override strong({ tokens }: Tokens.Strong): string {
100
+ const text = this.parser.parseInline(tokens);
101
+ return chalk.bold(text);
102
+ }
103
+
104
+ override em({ tokens }: Tokens.Em): string {
105
+ const text = this.parser.parseInline(tokens);
106
+ return chalk.italic(text);
107
+ }
108
+
109
+ override codespan({ text }: Tokens.Codespan): string {
110
+ return chalk.yellow(text);
111
+ }
112
+
113
+ override br(): string {
114
+ return "\n";
115
+ }
116
+
117
+ override del({ tokens }: Tokens.Del): string {
118
+ const text = this.parser.parseInline(tokens);
119
+ return chalk.strikethrough(text);
120
+ }
121
+
122
+ override link({ href, tokens }: Tokens.Link): string {
123
+ const text = this.parser.parseInline(tokens);
124
+ const linkText = chalk.blue.underline(text);
125
+ const hrefText = chalk.gray(`(${href})`);
126
+ return `${linkText} ${hrefText}`;
127
+ }
128
+
129
+ override image({ href, tokens, text }: Tokens.Image): string {
130
+ const alt = this.parser.parseInline(tokens) || text;
131
+ return chalk.gray(`![${alt}](${href})`);
132
+ }
133
+
134
+ override text(token: Tokens.Text | Tokens.Escape): string {
135
+ return "tokens" in token && token.tokens
136
+ ? this.parser.parseInline(token.tokens)
137
+ : unescapeHtml(token.text);
138
+ }
139
+ }
140
+
141
+ const renderer = new AnsiRenderer();
345
142
 
346
- // Markdown component using custom Ink-based renderer
143
+ // Markdown component using custom ANSI renderer
347
144
  export const Markdown = React.memo(({ children }: MarkdownProps) => {
348
- const tokens = useMemo(() => marked.lexer(children), [children]);
145
+ const ansiContent = useMemo(() => {
146
+ return marked.parse(children, {
147
+ renderer,
148
+ gfm: true,
149
+ breaks: true,
150
+ }) as string;
151
+ }, [children]);
349
152
 
350
153
  return (
351
154
  <Box flexDirection="column">
352
- <BlockRenderer tokens={tokens} />
155
+ <Text>{ansiContent.trim()}</Text>
353
156
  </Box>
354
157
  );
355
158
  });