@abacus-ai/cli 1.106.25007 → 2.0.0-canary.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 (200) hide show
  1. package/.oxlintrc.json +8 -0
  2. package/dist/index.mjs +12603 -0
  3. package/package.json +7 -39
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +9 -0
  6. package/src/__e2e__/README.md +196 -0
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
  10. package/src/__e2e__/conversation.e2e.test.tsx +56 -0
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
  12. package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
  13. package/src/__e2e__/helpers/test-helpers.ts +450 -0
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
  15. package/src/__e2e__/llm-models.e2e.test.ts +402 -0
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
  19. package/src/__e2e__/repl.e2e.test.tsx +78 -0
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
  23. package/src/args.ts +22 -0
  24. package/src/components/__tests__/react-compiler.test.tsx +78 -0
  25. package/src/components/__tests__/status-indicator.test.tsx +403 -0
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
  27. package/src/components/composer/agent-mode-indicator.tsx +63 -0
  28. package/src/components/composer/bash-runner.tsx +54 -0
  29. package/src/components/composer/commands/default-commands.tsx +615 -0
  30. package/src/components/composer/commands/handler.tsx +59 -0
  31. package/src/components/composer/commands/picker.tsx +273 -0
  32. package/src/components/composer/commands/registry.ts +233 -0
  33. package/src/components/composer/commands/types.ts +33 -0
  34. package/src/components/composer/context.tsx +88 -0
  35. package/src/components/composer/file-mention-picker.tsx +83 -0
  36. package/src/components/composer/help.tsx +44 -0
  37. package/src/components/composer/index.tsx +1006 -0
  38. package/src/components/composer/mentions.ts +57 -0
  39. package/src/components/composer/message-queue.tsx +70 -0
  40. package/src/components/composer/mode-panel.tsx +35 -0
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
  43. package/src/components/composer/modes/bash-handler.tsx +132 -0
  44. package/src/components/composer/modes/bash-renderer.tsx +175 -0
  45. package/src/components/composer/modes/default-handlers.tsx +33 -0
  46. package/src/components/composer/modes/index.ts +41 -0
  47. package/src/components/composer/modes/types.ts +21 -0
  48. package/src/components/composer/persistent-shell.ts +283 -0
  49. package/src/components/composer/process.ts +65 -0
  50. package/src/components/composer/types.ts +9 -0
  51. package/src/components/composer/use-mention-search.ts +68 -0
  52. package/src/components/error-boundry.tsx +60 -0
  53. package/src/components/exit-message.tsx +29 -0
  54. package/src/components/expanded-view.tsx +74 -0
  55. package/src/components/file-completion.tsx +127 -0
  56. package/src/components/header.tsx +47 -0
  57. package/src/components/logo.tsx +37 -0
  58. package/src/components/segments.tsx +356 -0
  59. package/src/components/status-indicator.tsx +306 -0
  60. package/src/components/tool-group-summary.tsx +263 -0
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
  62. package/src/components/tool-permissions/diff-preview.tsx +355 -0
  63. package/src/components/tool-permissions/index.ts +5 -0
  64. package/src/components/tool-permissions/permission-options.tsx +375 -0
  65. package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
  67. package/src/components/tools/agent/ask-user-question.tsx +101 -0
  68. package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
  69. package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
  70. package/src/components/tools/agent/handoff-to-main.tsx +27 -0
  71. package/src/components/tools/agent/subagent.tsx +37 -0
  72. package/src/components/tools/agent/todo-write.tsx +104 -0
  73. package/src/components/tools/browser/close-tab.tsx +58 -0
  74. package/src/components/tools/browser/computer.tsx +70 -0
  75. package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
  76. package/src/components/tools/browser/get-tab-content.tsx +51 -0
  77. package/src/components/tools/browser/navigate-to.tsx +59 -0
  78. package/src/components/tools/browser/new-tab.tsx +60 -0
  79. package/src/components/tools/browser/perform-action.tsx +63 -0
  80. package/src/components/tools/browser/refresh-tab.tsx +43 -0
  81. package/src/components/tools/browser/switch-tab.tsx +58 -0
  82. package/src/components/tools/filesystem/delete-file.tsx +104 -0
  83. package/src/components/tools/filesystem/edit.tsx +220 -0
  84. package/src/components/tools/filesystem/list-dir.tsx +78 -0
  85. package/src/components/tools/filesystem/read-file.tsx +180 -0
  86. package/src/components/tools/filesystem/upload-image.tsx +76 -0
  87. package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
  88. package/src/components/tools/index.ts +91 -0
  89. package/src/components/tools/mcp/mcp-tool.tsx +158 -0
  90. package/src/components/tools/search/fetch-url.tsx +73 -0
  91. package/src/components/tools/search/file-search.tsx +78 -0
  92. package/src/components/tools/search/grep.tsx +90 -0
  93. package/src/components/tools/search/semantic-search.tsx +66 -0
  94. package/src/components/tools/search/web-search.tsx +71 -0
  95. package/src/components/tools/shared/index.tsx +48 -0
  96. package/src/components/tools/shared/zod-coercion.ts +35 -0
  97. package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
  98. package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
  99. package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
  100. package/src/components/tools/types.ts +16 -0
  101. package/src/components/tools.tsx +66 -0
  102. package/src/components/ui/__tests__/divider.test.tsx +61 -0
  103. package/src/components/ui/__tests__/gradient.test.tsx +125 -0
  104. package/src/components/ui/__tests__/input.test.tsx +166 -0
  105. package/src/components/ui/__tests__/select.test.tsx +273 -0
  106. package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
  107. package/src/components/ui/blinking-indicator.tsx +25 -0
  108. package/src/components/ui/divider.tsx +162 -0
  109. package/src/components/ui/gradient.tsx +56 -0
  110. package/src/components/ui/input.tsx +228 -0
  111. package/src/components/ui/select.tsx +151 -0
  112. package/src/components/ui/shimmer.tsx +84 -0
  113. package/src/context/agent-mode.tsx +95 -0
  114. package/src/context/extension-file.tsx +136 -0
  115. package/src/context/network-activity.tsx +45 -0
  116. package/src/context/notification.tsx +62 -0
  117. package/src/context/shell-size.tsx +49 -0
  118. package/src/context/shell-title.tsx +38 -0
  119. package/src/entrypoints/print-mode.ts +312 -0
  120. package/src/entrypoints/repl.tsx +401 -0
  121. package/src/hooks/use-agent.ts +15 -0
  122. package/src/hooks/use-api-client.ts +1 -0
  123. package/src/hooks/use-available-height.ts +8 -0
  124. package/src/hooks/use-cleanup.ts +29 -0
  125. package/src/hooks/use-interrupt-manager.ts +242 -0
  126. package/src/hooks/use-models.ts +22 -0
  127. package/src/index.ts +217 -0
  128. package/src/lib/__tests__/ansi.test.ts +255 -0
  129. package/src/lib/__tests__/cli.test.ts +122 -0
  130. package/src/lib/__tests__/commands.test.ts +325 -0
  131. package/src/lib/__tests__/constants.test.ts +15 -0
  132. package/src/lib/__tests__/focusables.test.ts +25 -0
  133. package/src/lib/__tests__/fs.test.ts +231 -0
  134. package/src/lib/__tests__/markdown.test.tsx +348 -0
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
  136. package/src/lib/__tests__/mcpManagement.test.ts +38 -0
  137. package/src/lib/__tests__/path-paste.test.ts +144 -0
  138. package/src/lib/__tests__/path.test.ts +300 -0
  139. package/src/lib/__tests__/queries.test.ts +39 -0
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
  141. package/src/lib/__tests__/text-buffer.test.ts +328 -0
  142. package/src/lib/__tests__/text-utils.test.ts +32 -0
  143. package/src/lib/__tests__/timing.test.ts +78 -0
  144. package/src/lib/__tests__/utils.test.ts +238 -0
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
  146. package/src/lib/ansi.ts +150 -0
  147. package/src/lib/cli-push-server.ts +112 -0
  148. package/src/lib/cli.ts +44 -0
  149. package/src/lib/clipboard.ts +226 -0
  150. package/src/lib/command-utils.ts +93 -0
  151. package/src/lib/commands.ts +270 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/extension-connection.ts +181 -0
  154. package/src/lib/focusables.ts +7 -0
  155. package/src/lib/fs.ts +533 -0
  156. package/src/lib/markdown/code-block.tsx +63 -0
  157. package/src/lib/markdown/index.ts +4 -0
  158. package/src/lib/markdown/link.tsx +19 -0
  159. package/src/lib/markdown/markdown.tsx +372 -0
  160. package/src/lib/markdown/types.ts +15 -0
  161. package/src/lib/mcpCommandHandler.ts +121 -0
  162. package/src/lib/mcpManagement.ts +44 -0
  163. package/src/lib/path-paste.ts +185 -0
  164. package/src/lib/path.ts +179 -0
  165. package/src/lib/queries.ts +15 -0
  166. package/src/lib/standaloneMcpService.ts +688 -0
  167. package/src/lib/status-utils.ts +237 -0
  168. package/src/lib/test-utils.tsx +72 -0
  169. package/src/lib/text-buffer.ts +2415 -0
  170. package/src/lib/text-utils.ts +272 -0
  171. package/src/lib/timing.ts +63 -0
  172. package/src/lib/types.ts +295 -0
  173. package/src/lib/utils.ts +182 -0
  174. package/src/lib/vim-buffer-actions.ts +732 -0
  175. package/src/providers/agent.tsx +1075 -0
  176. package/src/providers/api-client.tsx +43 -0
  177. package/src/services/logger.ts +85 -0
  178. package/src/terminal/detection.ts +187 -0
  179. package/src/terminal/exit.ts +279 -0
  180. package/src/terminal/notification.ts +83 -0
  181. package/src/terminal/progress.ts +201 -0
  182. package/src/terminal/setup.ts +797 -0
  183. package/src/terminal/suspend.ts +58 -0
  184. package/src/terminal/types.ts +51 -0
  185. package/src/theme/context.tsx +57 -0
  186. package/src/theme/index.ts +4 -0
  187. package/src/theme/themed.tsx +35 -0
  188. package/src/theme/themes.json +546 -0
  189. package/src/theme/types.ts +110 -0
  190. package/src/tools/types.ts +59 -0
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
  192. package/src/tools/utils/tool-ui-components.tsx +631 -0
  193. package/src/tools/utils/zod-coercion.ts +35 -0
  194. package/tsconfig.json +11 -0
  195. package/tsconfig.node.json +29 -0
  196. package/tsconfig.test.json +27 -0
  197. package/tsdown.config.ts +17 -0
  198. package/vitest.config.ts +76 -0
  199. package/README.md +0 -28
  200. package/dist/index.js +0 -26
@@ -0,0 +1,372 @@
1
+ import { View, Text } from "@codellm/jar";
2
+ import { marked, type Token, type Tokens } from "marked";
3
+ import { Fragment, useDeferredValue, useMemo } from "react";
4
+ import remend from "remend";
5
+
6
+ import type { MarkdownProps } from "./types.js";
7
+
8
+ import { useTheme } from "../../theme/index.js";
9
+ import { CodeBlock } from "./code-block.js";
10
+ import { Link } from "./link.js";
11
+
12
+ /**
13
+ * Helper to extract plain text from tokens for alignment or other purposes.
14
+ */
15
+ function extractText(token: Token | Tokens.TableCell): string {
16
+ if ("text" in token && typeof token.text === "string") {
17
+ return token.text;
18
+ }
19
+ if ("tokens" in token && Array.isArray(token.tokens)) {
20
+ return token.tokens.map((t) => extractText(t)).join("");
21
+ }
22
+ return "";
23
+ }
24
+
25
+ /**
26
+ * Type guard to check if a token has a checked property (for checkbox list items)
27
+ */
28
+ function hasCheckedProperty(token: Token): token is Token & { checked: boolean } {
29
+ return "checked" in token && typeof (token as { checked?: unknown }).checked === "boolean";
30
+ }
31
+
32
+ /**
33
+ * Renders tokens that are guaranteed to be inline-safe (no View components).
34
+ */
35
+ function renderInlineTokens(
36
+ tokens: Token[] | undefined,
37
+ colors: ReturnType<typeof useTheme>["colors"],
38
+ isDimItalic = false,
39
+ ): React.ReactNode {
40
+ if (!tokens) return null;
41
+
42
+ return tokens.map((token, i) => (
43
+ <Fragment key={i}>{renderInlineToken(token, colors, isDimItalic)}</Fragment>
44
+ ));
45
+ }
46
+
47
+ /**
48
+ * Renders a single inline-safe token.
49
+ */
50
+ function renderInlineToken(
51
+ token: Token,
52
+ colors: ReturnType<typeof useTheme>["colors"],
53
+ isDimItalic = false,
54
+ ): React.ReactNode {
55
+ const wrapWithStyle = (node: React.ReactNode) => {
56
+ if (isDimItalic) {
57
+ return (
58
+ <Text dimColor italic>
59
+ {node}
60
+ </Text>
61
+ );
62
+ }
63
+ return node;
64
+ };
65
+
66
+ switch (token.type) {
67
+ case "strong": {
68
+ return <Text bold>{renderInlineTokens(token.tokens, colors, isDimItalic)}</Text>;
69
+ }
70
+
71
+ case "em": {
72
+ return <Text italic>{renderInlineTokens(token.tokens, colors, isDimItalic)}</Text>;
73
+ }
74
+
75
+ case "del": {
76
+ return <Text strikethrough>{renderInlineTokens(token.tokens, colors, isDimItalic)}</Text>;
77
+ }
78
+
79
+ case "codespan": {
80
+ return wrapWithStyle(
81
+ <Text color={colors.primary} dimColor>
82
+ {token.text}
83
+ </Text>,
84
+ );
85
+ }
86
+
87
+ case "link": {
88
+ return wrapWithStyle(<Link href={token.href}>{extractText(token)}</Link>);
89
+ }
90
+
91
+ case "image": {
92
+ const imageToken = token as Tokens.Image;
93
+ // Show alt text with image indicator, or URL if no alt
94
+ const displayText = imageToken.text || imageToken.href;
95
+ return wrapWithStyle(
96
+ <Text dimColor>
97
+ {displayText} {imageToken.href !== displayText && `(${imageToken.href})`}
98
+ </Text>,
99
+ );
100
+ }
101
+
102
+ case "checkbox": {
103
+ const isChecked = hasCheckedProperty(token) && token.checked === true;
104
+ return wrapWithStyle(<Text color={colors.secondary}>{isChecked ? "[✓]" : "[ ]"}</Text>);
105
+ }
106
+
107
+ case "text": {
108
+ if (token.tokens) {
109
+ return renderInlineTokens(token.tokens, colors, isDimItalic);
110
+ }
111
+ return wrapWithStyle(token.text);
112
+ }
113
+
114
+ case "br":
115
+ return "\n";
116
+
117
+ default:
118
+ return wrapWithStyle(extractText(token));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Renders tokens that may contain block-level components (View).
124
+ * Headings get double newline, paragraphs/code/lists get single.
125
+ */
126
+ function renderBlockToken(
127
+ token: Token,
128
+ colors: ReturnType<typeof useTheme>["colors"],
129
+ listDepth = 0,
130
+ isDimItalic = false,
131
+ ): React.ReactNode {
132
+ switch (token.type) {
133
+ case "heading": {
134
+ // Headings get blank line after (double newline in Claude CLI)
135
+ return (
136
+ <View key={`heading-${token.depth}`} flexDirection="column">
137
+ <Text
138
+ bold
139
+ color={token.depth <= 2 ? colors.primary : colors.secondary}
140
+ dimColor={token.depth > 2}
141
+ >
142
+ {renderInlineTokens(token.tokens, colors, isDimItalic)}
143
+ </Text>
144
+ </View>
145
+ );
146
+ }
147
+
148
+ case "paragraph": {
149
+ // Paragraphs get single newline after (handled by Ink's natural line flow)
150
+ return (
151
+ <View key="paragraph" flexDirection="column">
152
+ <Text>{renderInlineTokens(token.tokens, colors, isDimItalic)}</Text>
153
+ </View>
154
+ );
155
+ }
156
+
157
+ case "code": {
158
+ // Code blocks get single newline after
159
+ return (
160
+ <CodeBlock key="code" language={token.lang}>
161
+ {token.text}
162
+ </CodeBlock>
163
+ );
164
+ }
165
+
166
+ case "blockquote": {
167
+ // Blockquotes: no extra spacing, just dim/italic content (like Claude CLI)
168
+ return (
169
+ <View
170
+ key="blockquote"
171
+ borderRight={false}
172
+ borderTop={false}
173
+ borderBottom={false}
174
+ borderStyle="double"
175
+ borderColor={colors.border}
176
+ paddingLeft={1}
177
+ flexDirection="column"
178
+ >
179
+ {token.tokens?.map((t, i) => (
180
+ <View key={i}>{renderBlockToken(t, colors, listDepth, true)}</View>
181
+ ))}
182
+ </View>
183
+ );
184
+ }
185
+
186
+ case "list": {
187
+ const listToken = token as Tokens.List;
188
+ // Lists: each item gets single newline
189
+ return (
190
+ <View key="list" flexDirection="column">
191
+ {listToken.items.map((item, index) => {
192
+ const number = token.ordered ? (Number(token.start) || 1) + index : null;
193
+ const isTask = item.task === true;
194
+ const isChecked = item.checked === true;
195
+
196
+ // Filter out checkbox tokens since we render them manually
197
+ const contentTokens = item.tokens?.filter((t) => t.type !== "checkbox") || [];
198
+
199
+ return (
200
+ <View key={index} flexDirection="row">
201
+ <Text color={colors.secondary} dimColor>
202
+ {isTask ? (isChecked ? "[✓] " : "[ ] ") : number !== null ? `${number}. ` : "• "}
203
+ </Text>
204
+ <View flexGrow={1} flexDirection="column">
205
+ {contentTokens.map((t, i) => (
206
+ <View key={i}>{renderBlockToken(t, colors, listDepth + 1, isDimItalic)}</View>
207
+ ))}
208
+ </View>
209
+ </View>
210
+ );
211
+ })}
212
+ </View>
213
+ );
214
+ }
215
+
216
+ case "table": {
217
+ const tableToken = token as Tokens.Table;
218
+
219
+ // Calculate column widths (like Claude CLI)
220
+ const columnWidths = tableToken.header.map((headerCell, i) => {
221
+ let maxWidth = extractText(headerCell).length;
222
+ for (const row of tableToken.rows) {
223
+ maxWidth = Math.max(maxWidth, extractText(row[i]).length);
224
+ }
225
+ return Math.max(maxWidth, 3);
226
+ });
227
+
228
+ // Tables get extra newline at end (like Claude CLI)
229
+ return (
230
+ <View key="table" flexDirection="column">
231
+ <View
232
+ flexDirection="row"
233
+ borderTop={false}
234
+ borderLeft={false}
235
+ borderRight={false}
236
+ borderStyle="single"
237
+ borderColor={colors.border}
238
+ alignSelf="flex-start"
239
+ >
240
+ {tableToken.header.map((cell, i) => {
241
+ const align = tableToken.align?.[i] || null;
242
+ return (
243
+ <View
244
+ key={i}
245
+ width={columnWidths[i] + 2}
246
+ paddingX={1}
247
+ justifyContent={
248
+ align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
249
+ }
250
+ >
251
+ <Text bold color={colors.primary}>
252
+ {renderInlineTokens(cell.tokens, colors, isDimItalic)}
253
+ </Text>
254
+ </View>
255
+ );
256
+ })}
257
+ </View>
258
+ {tableToken.rows.map((row, rowIndex) => (
259
+ <View key={rowIndex} flexDirection="row">
260
+ {row.map((cell, cellIndex) => {
261
+ const align = tableToken.align?.[cellIndex] || null;
262
+ return (
263
+ <View
264
+ key={cellIndex}
265
+ width={columnWidths[cellIndex] + 2}
266
+ paddingX={1}
267
+ justifyContent={
268
+ align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"
269
+ }
270
+ >
271
+ <Text>{renderInlineTokens(cell.tokens, colors, isDimItalic)}</Text>
272
+ </View>
273
+ );
274
+ })}
275
+ </View>
276
+ ))}
277
+ </View>
278
+ );
279
+ }
280
+
281
+ case "hr":
282
+ // HR: just the line, no extra spacing (like Claude CLI returns just "---")
283
+ return (
284
+ <View key="hr">
285
+ <Text dimColor>{"─".repeat(50)}</Text>
286
+ </View>
287
+ );
288
+
289
+ case "space":
290
+ // Space tokens become single newline (like Claude CLI)
291
+ return null;
292
+
293
+ default:
294
+ return (
295
+ <View flexDirection="column">
296
+ <Text>{renderInlineToken(token, colors, isDimItalic)}</Text>
297
+ </View>
298
+ );
299
+ }
300
+ }
301
+
302
+ /**
303
+ * A type-safe terminal markdown renderer using marked and Jar components.
304
+ * Spacing rules:
305
+ * - Headings: content + EOL + EOL (blank line after)
306
+ * - Paragraphs: content + EOL (single newline)
307
+ * - Code: content + EOL
308
+ * - Space: EOL
309
+ * - Lists: each item ends with EOL
310
+ * - Tables: content + EOL
311
+ */
312
+ export function Markdown({ children }: MarkdownProps) {
313
+ const { colors } = useTheme();
314
+
315
+ // Defer markdown content to prevent blocking input during parsing
316
+ const deferredChildren = useDeferredValue(children);
317
+ const isRendering = children !== deferredChildren;
318
+
319
+ const tokens = useMemo(() => {
320
+ try {
321
+ // Use remend to complete incomplete markdown during streaming
322
+ const completedMarkdown = remend(deferredChildren);
323
+ // Enable GFM and breaks for proper line break handling
324
+ return marked.lexer(completedMarkdown, { gfm: true, breaks: true });
325
+ } catch {
326
+ return [];
327
+ }
328
+ }, [deferredChildren]);
329
+
330
+ // Process tokens with Claude CLI-style spacing
331
+ const renderedTokens = useMemo(() => {
332
+ const result: React.ReactNode[] = [];
333
+ const totalTokens = tokens.length;
334
+
335
+ tokens.forEach((token, index) => {
336
+ const isLast = index === totalTokens - 1;
337
+
338
+ // Space tokens add a blank line (like Claude CLI returns EOL for space)
339
+ if (token.type === "space") {
340
+ result.push(<View key={`space-${index}`} height={1} />);
341
+ return;
342
+ }
343
+
344
+ // Render the token
345
+ result.push(
346
+ <View key={index} flexDirection="column">
347
+ {renderBlockToken(token, colors)}
348
+ </View>,
349
+ );
350
+
351
+ // Add trailing blank line after headings (like Claude CLI's EOL + EOL)
352
+ if (token.type === "heading" && !isLast) {
353
+ result.push(<View key={`heading-space-${index}`} height={1} />);
354
+ }
355
+ });
356
+
357
+ return result;
358
+ }, [tokens, colors]);
359
+
360
+ return (
361
+ <View flexDirection="column">
362
+ {isRendering && (
363
+ <View>
364
+ <Text dimColor>Rendering markdown...</Text>
365
+ </View>
366
+ )}
367
+ {renderedTokens}
368
+ </View>
369
+ );
370
+ }
371
+
372
+ Markdown.displayName = "Markdown";
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface MarkdownProps {
4
+ children: string;
5
+ }
6
+
7
+ export interface CodeBlockProps {
8
+ children?: string;
9
+ language?: string;
10
+ }
11
+
12
+ export interface LinkProps {
13
+ href: string;
14
+ children: ReactNode;
15
+ }
@@ -0,0 +1,121 @@
1
+ import type { McpManager } from "@codellm/agent";
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { McpManagementService } from "./mcpManagement.js";
8
+
9
+ function logMcpError(context: string, error: unknown): void {
10
+ try {
11
+ const logDir = path.join(os.tmpdir(), "abacus-tui-logs");
12
+ if (!fs.existsSync(logDir)) {
13
+ fs.mkdirSync(logDir, { recursive: true });
14
+ }
15
+ const logFile = path.join(logDir, "mcp-errors.log");
16
+ const timestamp = new Date().toISOString();
17
+ const errorMessage = error instanceof Error ? error.message : String(error);
18
+ const errorStack = error instanceof Error ? error.stack : "";
19
+ const logEntry = `[${timestamp}] ${context}: ${errorMessage}\n${errorStack ? `Stack: ${errorStack}\n` : ""}\n`;
20
+ fs.appendFileSync(logFile, logEntry, "utf8");
21
+ } catch {
22
+ // If logging fails, just log to console as fallback
23
+ console.error(`[MCP] ${context}:`, error);
24
+ }
25
+ }
26
+
27
+ export class McpCommandHandler {
28
+ private mcpService: McpManagementService;
29
+ private mcp?: McpManager;
30
+
31
+ constructor(mcp?: McpManager) {
32
+ this.mcpService = new McpManagementService();
33
+ this.mcp = mcp;
34
+ }
35
+
36
+ async listServers(): Promise<string> {
37
+ const servers = await this.mcpService.listServers();
38
+ // Update server status with real-time connection status from McpManager
39
+ if (this.mcp) {
40
+ const liveServers = this.mcp.listServers();
41
+ for (const server of servers) {
42
+ const live = liveServers.find(
43
+ (s) => s.config.id === server.id || s.config.id === server.name,
44
+ );
45
+ server.status = live ? "running" : "stopped";
46
+ }
47
+ }
48
+ return this.mcpService.formatServerList(servers);
49
+ }
50
+
51
+ async addServer(name: string, commandOrUrl: string, extraArgs: string[]): Promise<void> {
52
+ await this.mcpService.addServerFromUrl(name, commandOrUrl, extraArgs);
53
+ // Refresh live connections in background
54
+ if (this.mcp) {
55
+ this.mcp.refresh().catch((error: unknown) => {
56
+ logMcpError(`Failed to refresh MCP connections after adding server '${name}'`, error);
57
+ });
58
+ }
59
+ }
60
+
61
+ async removeServer(name: string): Promise<void> {
62
+ await this.mcpService.removeServer(name);
63
+ // Disconnect live server in background
64
+ if (this.mcp) {
65
+ this.mcp.removeServer(name).catch((error: unknown) => {
66
+ logMcpError(`Failed to disconnect MCP server '${name}'`, error);
67
+ });
68
+ }
69
+ }
70
+
71
+ async getServer(name: string): Promise<string | null> {
72
+ const server = await this.mcpService.getServer(name);
73
+ if (server) {
74
+ // Update server status with real-time connection status from McpManager
75
+ if (this.mcp) {
76
+ const liveServers = this.mcp.listServers();
77
+ const live = liveServers.find(
78
+ (s) => s.config.id === server.id || s.config.id === server.name,
79
+ );
80
+ server.status = live ? "running" : "stopped";
81
+ }
82
+ return this.mcpService.formatServerInfo(server);
83
+ }
84
+ return null;
85
+ }
86
+
87
+ async addServerFromJson(name: string, jsonConfig: string): Promise<void> {
88
+ await this.mcpService.addServerFromJson(name, jsonConfig);
89
+ // Refresh live connections in background
90
+ if (this.mcp) {
91
+ this.mcp.refresh().catch((error: unknown) => {
92
+ logMcpError(
93
+ `Failed to refresh MCP connections after adding server '${name}' from JSON`,
94
+ error,
95
+ );
96
+ });
97
+ }
98
+ }
99
+
100
+ formatHelp(): string {
101
+ return helpText;
102
+ }
103
+ }
104
+
105
+ export const helpText = `
106
+ MCP (Model Context Protocol) Commands:
107
+
108
+ /mcp list List configured MCP servers
109
+ /mcp add Add a server
110
+ /mcp remove Remove an MCP server
111
+ /mcp get Get details about an MCP server
112
+ /mcp add-json Add an MCP server with JSON configuration
113
+
114
+ Example - Add GitHub MCP Server:
115
+ /mcp add-json github '{"command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"your_github_token_here"}}'
116
+
117
+ TUI Features:
118
+ - Full MCP server management from command line
119
+ - All commands work independently in TUI
120
+ - Direct file-based configuration access
121
+ `;
@@ -0,0 +1,44 @@
1
+ import type { IMcpServerInfo, IMcpManagementService, IMcpServerConfig } from "./types.js";
2
+
3
+ import { StandaloneMcpService } from "./standaloneMcpService.js";
4
+
5
+ export class McpManagementService implements IMcpManagementService {
6
+ private standaloneService: StandaloneMcpService;
7
+
8
+ constructor() {
9
+ // Always use standalone service - no workbench dependencies
10
+ this.standaloneService = new StandaloneMcpService();
11
+ }
12
+
13
+ async listServers(): Promise<IMcpServerInfo[]> {
14
+ return this.standaloneService.listServers();
15
+ }
16
+
17
+ async addServer(name: string, config: IMcpServerConfig): Promise<void> {
18
+ return this.standaloneService.addServer(name, config);
19
+ }
20
+
21
+ async removeServer(name: string): Promise<void> {
22
+ return this.standaloneService.removeServer(name);
23
+ }
24
+
25
+ async getServer(name: string): Promise<IMcpServerInfo | undefined> {
26
+ return this.standaloneService.getServer(name);
27
+ }
28
+
29
+ async addServerFromJson(name: string, jsonConfig: string): Promise<void> {
30
+ return this.standaloneService.addServerFromJson(name, jsonConfig);
31
+ }
32
+
33
+ async addServerFromUrl(name: string, url: string, args?: string[]): Promise<void> {
34
+ return this.standaloneService.addServerFromUrl(name, url, args);
35
+ }
36
+
37
+ formatServerInfo(server: IMcpServerInfo): string {
38
+ return this.standaloneService.formatServerInfo(server);
39
+ }
40
+
41
+ formatServerList(servers: IMcpServerInfo[]): string {
42
+ return this.standaloneService.formatServerList(servers);
43
+ }
44
+ }