aegiscode 3.1.8 → 3.1.10

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 (206) hide show
  1. package/README.md +23 -15
  2. package/dist/main.js +549 -552
  3. package/package.json +4 -2
  4. package/src/agent/Agent.ts +903 -0
  5. package/src/agent/SimpleAgent.ts +48 -0
  6. package/src/agent/index.ts +54 -0
  7. package/src/agent/orchestrator/AppBuilder.ts +443 -0
  8. package/src/agent/orchestrator/CouncilAgent.ts +310 -0
  9. package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
  10. package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
  11. package/src/agent/orchestrator/index.ts +38 -0
  12. package/src/agent/orchestrator/utils.ts +397 -0
  13. package/src/agent/pricing.ts +115 -0
  14. package/src/agent/router.ts +74 -0
  15. package/src/agent/routerStats.ts +121 -0
  16. package/src/agent/types.ts +318 -0
  17. package/src/auth/login.ts +383 -0
  18. package/src/cli/config.ts +189 -0
  19. package/src/cli/index.ts +17 -0
  20. package/src/cli/middleware.ts +119 -0
  21. package/src/cli/types.ts +75 -0
  22. package/src/config/ConfigManager.ts +587 -0
  23. package/src/config/index.ts +7 -0
  24. package/src/config/types.ts +584 -0
  25. package/src/context/CompactionService.ts +300 -0
  26. package/src/context/ContextManager.ts +450 -0
  27. package/src/context/FileAnalyzer.ts +267 -0
  28. package/src/context/TokenCounter.ts +265 -0
  29. package/src/context/index.ts +27 -0
  30. package/src/context/storage/CacheStore.ts +176 -0
  31. package/src/context/storage/JSONLStore.ts +201 -0
  32. package/src/context/storage/MemoryStore.ts +205 -0
  33. package/src/context/storage/PersistentStore.ts +327 -0
  34. package/src/context/storage/index.ts +9 -0
  35. package/src/context/storage/pathUtils.ts +114 -0
  36. package/src/context/test.ts +309 -0
  37. package/src/context/types.ts +268 -0
  38. package/src/hooks/HookExecutor.ts +434 -0
  39. package/src/hooks/HookManager.ts +596 -0
  40. package/src/hooks/HookService.ts +269 -0
  41. package/src/hooks/Matcher.ts +157 -0
  42. package/src/hooks/index.ts +63 -0
  43. package/src/hooks/types.ts +424 -0
  44. package/src/main.tsx +596 -0
  45. package/src/mcp/HealthMonitor.ts +150 -0
  46. package/src/mcp/McpClient.ts +491 -0
  47. package/src/mcp/McpRegistry.ts +321 -0
  48. package/src/mcp/createMcpTool.ts +251 -0
  49. package/src/mcp/index.ts +15 -0
  50. package/src/mcp/server.ts +334 -0
  51. package/src/mcp/test-server.ts +88 -0
  52. package/src/mcp/test.ts +372 -0
  53. package/src/mcp/types.ts +247 -0
  54. package/src/memory/AgentMemoryBus.ts +432 -0
  55. package/src/memory/CloudSync.ts +99 -0
  56. package/src/memory/DriveSync.ts +106 -0
  57. package/src/memory/SharedMemory.ts +951 -0
  58. package/src/memory/index.ts +14 -0
  59. package/src/memory/machineFingerprint.ts +40 -0
  60. package/src/orchestrator/SubAgentMetadata.ts +136 -0
  61. package/src/prompts/builder.ts +213 -0
  62. package/src/prompts/default.ts +144 -0
  63. package/src/prompts/index.ts +16 -0
  64. package/src/prompts/plan.ts +64 -0
  65. package/src/prompts/test.ts +78 -0
  66. package/src/services/AnthropicChatService.ts +341 -0
  67. package/src/services/ChatService.ts +347 -0
  68. package/src/services/ClaudeCliChatService.ts +256 -0
  69. package/src/services/CloudSync.ts +168 -0
  70. package/src/services/CostLedger.ts +211 -0
  71. package/src/services/Heartbeat.ts +135 -0
  72. package/src/services/LearningCollector.ts +291 -0
  73. package/src/services/OllamaInstaller.ts +342 -0
  74. package/src/services/VersionChecker.ts +445 -0
  75. package/src/services/index.ts +57 -0
  76. package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
  77. package/src/services/streaming/RenderingProfile.ts +90 -0
  78. package/src/services/streaming/StreamEventParser.ts +181 -0
  79. package/src/services/streaming/ThrottledRenderer.ts +139 -0
  80. package/src/services/streaming/TranscriptBuffer.ts +574 -0
  81. package/src/services/streaming/eventStatusMap.ts +52 -0
  82. package/src/services/streaming/index.ts +46 -0
  83. package/src/services/streaming/renderFormatting.ts +79 -0
  84. package/src/services/streaming/types.ts +234 -0
  85. package/src/skills/SkillLoader.ts +126 -0
  86. package/src/skills/SkillRegistry.ts +366 -0
  87. package/src/skills/index.ts +48 -0
  88. package/src/skills/types.ts +146 -0
  89. package/src/slash-commands/billing.ts +70 -0
  90. package/src/slash-commands/build.ts +413 -0
  91. package/src/slash-commands/builtinCommands.ts +2733 -0
  92. package/src/slash-commands/clone.ts +242 -0
  93. package/src/slash-commands/council.ts +125 -0
  94. package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
  95. package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
  96. package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
  97. package/src/slash-commands/custom/index.ts +7 -0
  98. package/src/slash-commands/debate.ts +254 -0
  99. package/src/slash-commands/gmail.ts +105 -0
  100. package/src/slash-commands/index.ts +388 -0
  101. package/src/slash-commands/mcpCommand.ts +205 -0
  102. package/src/slash-commands/types.ts +201 -0
  103. package/src/store/index.ts +76 -0
  104. package/src/store/selectors.ts +246 -0
  105. package/src/store/slices/appSlice.ts +205 -0
  106. package/src/store/slices/commandSlice.ts +115 -0
  107. package/src/store/slices/configSlice.ts +46 -0
  108. package/src/store/slices/focusSlice.ts +64 -0
  109. package/src/store/slices/index.ts +9 -0
  110. package/src/store/slices/sessionSlice.ts +424 -0
  111. package/src/store/streaming-buffer.ts +425 -0
  112. package/src/store/test.ts +296 -0
  113. package/src/store/types.ts +274 -0
  114. package/src/store/vanilla.ts +186 -0
  115. package/src/tools/builtin/bash.ts +236 -0
  116. package/src/tools/builtin/council.ts +105 -0
  117. package/src/tools/builtin/edit.ts +213 -0
  118. package/src/tools/builtin/glob.ts +136 -0
  119. package/src/tools/builtin/grep.ts +263 -0
  120. package/src/tools/builtin/index.ts +61 -0
  121. package/src/tools/builtin/memory.ts +66 -0
  122. package/src/tools/builtin/read.ts +168 -0
  123. package/src/tools/builtin/skill.ts +97 -0
  124. package/src/tools/builtin/snapshot.ts +40 -0
  125. package/src/tools/builtin/task.ts +106 -0
  126. package/src/tools/builtin/write.ts +134 -0
  127. package/src/tools/createTool.ts +221 -0
  128. package/src/tools/execution/ExecutionPipeline.ts +263 -0
  129. package/src/tools/execution/index.ts +40 -0
  130. package/src/tools/execution/stages/CacheStage.ts +131 -0
  131. package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
  132. package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
  133. package/src/tools/execution/stages/ExecutionStage.ts +48 -0
  134. package/src/tools/execution/stages/FormattingStage.ts +44 -0
  135. package/src/tools/execution/stages/HookStage.ts +72 -0
  136. package/src/tools/execution/stages/PermissionStage.ts +287 -0
  137. package/src/tools/execution/stages/PostHookStage.ts +71 -0
  138. package/src/tools/execution/stages/index.ts +12 -0
  139. package/src/tools/execution/test.ts +266 -0
  140. package/src/tools/execution/types.ts +273 -0
  141. package/src/tools/index.ts +81 -0
  142. package/src/tools/registry.ts +304 -0
  143. package/src/tools/schemas.ts +109 -0
  144. package/src/tools/test.ts +220 -0
  145. package/src/tools/types.ts +175 -0
  146. package/src/tools/validation/PermissionChecker.ts +242 -0
  147. package/src/tools/validation/SensitiveFileDetector.ts +210 -0
  148. package/src/tools/validation/index.ts +11 -0
  149. package/src/ui/App.tsx +166 -0
  150. package/src/ui/components/AegisInterface.tsx +484 -0
  151. package/src/ui/components/common/ChatSearch.tsx +150 -0
  152. package/src/ui/components/common/ErrorBoundary.tsx +82 -0
  153. package/src/ui/components/common/ExitMessage.tsx +120 -0
  154. package/src/ui/components/common/LoadingIndicator.tsx +49 -0
  155. package/src/ui/components/common/index.ts +6 -0
  156. package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
  157. package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
  158. package/src/ui/components/dialog/SetupWizard.tsx +297 -0
  159. package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
  160. package/src/ui/components/dialog/index.ts +8 -0
  161. package/src/ui/components/index.ts +28 -0
  162. package/src/ui/components/input/CommandSuggestions.tsx +139 -0
  163. package/src/ui/components/input/CustomTextInput.tsx +220 -0
  164. package/src/ui/components/input/InputArea.tsx +361 -0
  165. package/src/ui/components/input/PromptSuggestions.tsx +66 -0
  166. package/src/ui/components/input/index.ts +6 -0
  167. package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
  168. package/src/ui/components/layout/ContextBar.tsx +79 -0
  169. package/src/ui/components/layout/MessageArea.tsx +96 -0
  170. package/src/ui/components/layout/MessageList.tsx +647 -0
  171. package/src/ui/components/layout/MessageSeparator.tsx +26 -0
  172. package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
  173. package/src/ui/components/layout/index.ts +7 -0
  174. package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
  175. package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
  176. package/src/ui/components/markdown/index.ts +8 -0
  177. package/src/ui/components/markdown/parser.ts +336 -0
  178. package/src/ui/components/markdown/types.ts +66 -0
  179. package/src/ui/focus/FocusManager.ts +137 -0
  180. package/src/ui/focus/index.ts +13 -0
  181. package/src/ui/focus/types.ts +54 -0
  182. package/src/ui/focus/useFocus.ts +75 -0
  183. package/src/ui/hooks/index.ts +11 -0
  184. package/src/ui/hooks/useAgent.ts +284 -0
  185. package/src/ui/hooks/useCommandHistory.ts +87 -0
  186. package/src/ui/hooks/useCommandProcessor.ts +443 -0
  187. package/src/ui/hooks/useConfirmation.ts +99 -0
  188. package/src/ui/hooks/useCtrlCHandler.ts +100 -0
  189. package/src/ui/hooks/useInputBuffer.ts +122 -0
  190. package/src/ui/hooks/useTerminalSize.ts +68 -0
  191. package/src/ui/hooks/useTerminalWidth.ts +5 -0
  192. package/src/ui/hooks/useWindowedList.ts +118 -0
  193. package/src/ui/render-debugger.ts +621 -0
  194. package/src/ui/test.ts +189 -0
  195. package/src/ui/themes/ThemeManager.ts +332 -0
  196. package/src/ui/themes/aegisTheme.ts +87 -0
  197. package/src/ui/themes/darkTheme.ts +87 -0
  198. package/src/ui/themes/defaultTheme.ts +85 -0
  199. package/src/ui/themes/index.ts +10 -0
  200. package/src/ui/themes/lightTheme.ts +89 -0
  201. package/src/ui/themes/popularThemes.ts +187 -0
  202. package/src/ui/themes/types.ts +130 -0
  203. package/src/utils/clipboard.ts +48 -0
  204. package/src/utils/debug.ts +43 -0
  205. package/src/utils/environment.ts +68 -0
  206. package/src/utils/index.ts +10 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * WelcomeMessage — ASCII art logo + available commands.
3
+ */
4
+
5
+ import React, { useEffect, useState } from 'react';
6
+ import { Box, Text } from 'ink';
7
+ import { themeManager } from '../../themes/index.js';
8
+ import pkg from '../../../../package.json' with { type: 'json' };
9
+
10
+ interface WelcomeMessageProps {
11
+ terminalWidth: number;
12
+ }
13
+
14
+ const COMMANDS = [
15
+ { cmd: '/help', desc: 'all commands' },
16
+ { cmd: '/model', desc: 'switch AI model' },
17
+ { cmd: '/theme', desc: 'change appearance' },
18
+ { cmd: '/copy', desc: 'copy code/text to clipboard' },
19
+ { cmd: '/compact', desc: 'compress context' },
20
+ ];
21
+
22
+ /** Keeps fade-in state alive across remounts (e.g. selector closing) */
23
+ const fadePlayedRef = { current: false };
24
+
25
+ const FadeInCommand: React.FC<{ cmd: string; desc: string; delayMs: number }> = ({ cmd, desc, delayMs }) => {
26
+ const theme = themeManager.getTheme();
27
+ const [visible, setVisible] = useState(fadePlayedRef.current);
28
+ useEffect(() => {
29
+ if (fadePlayedRef.current) return;
30
+ const t = setTimeout(() => {
31
+ fadePlayedRef.current = true;
32
+ setVisible(true);
33
+ }, delayMs);
34
+ return () => clearTimeout(t);
35
+ }, [delayMs]);
36
+ if (!visible) return null;
37
+ return (
38
+ <Box>
39
+ <Text color={theme.colors.text.muted} dimColor>{cmd.padEnd(12)}{desc}</Text>
40
+ </Box>
41
+ );
42
+ };
43
+
44
+ export const WelcomeMessage: React.FC<WelcomeMessageProps> = React.memo(({ terminalWidth }) => {
45
+ const theme = themeManager.getTheme();
46
+ const cmdBaseT = 80;
47
+ const footerT = cmdBaseT + COMMANDS.length * 100 + 80;
48
+
49
+ return (
50
+ <Box flexDirection="column" paddingX={0} paddingY={0} marginBottom={1}>
51
+ {/* ASCII art logo — kept in sync with assets/demo.svg */}
52
+ <Box flexDirection="column">
53
+ <Text color={theme.colors.primary} bold>{`
54
+ ╔═╗╔═╗╔═╗╦╔═╗
55
+ ╠═╣║╣ ║ ╦║╚═╗
56
+ ╩ ╩╚═╝╚═╝╩╚═╝`}</Text>
57
+ <Box marginLeft={2}>
58
+ <Text color={theme.colors.text.muted} dimColor>v{pkg.version}</Text>
59
+ </Box>
60
+ <Box marginLeft={2}>
61
+ <Text color={theme.colors.primary} bold>ÆGIS </Text>
62
+ <Text color={theme.colors.text.muted} dimColor>· terminal coding agent</Text>
63
+ </Box>
64
+ <Box marginLeft={2}>
65
+ <Text color={theme.colors.text.muted} dimColor>──────────────────────────────────────</Text>
66
+ </Box>
67
+ </Box>
68
+
69
+ {/* Commands */}
70
+ <Box flexDirection="column" marginTop={0} marginLeft={2}>
71
+ {COMMANDS.map(({ cmd, desc }, i) => (
72
+ <FadeInCommand key={cmd} cmd={cmd} desc={desc} delayMs={cmdBaseT + i * 100} />
73
+ ))}
74
+ </Box>
75
+
76
+ {/* Footer */}
77
+ <Box marginTop={0} marginLeft={2}>
78
+ <Text color={theme.colors.text.muted} dimColor>Ctrl+Z exit · Ctrl+F search · Alt+C copy last</Text>
79
+ </Box>
80
+
81
+ {/* Claude Pro/Max tip — only shown when subscription auth isn't already set up */}
82
+ {!process.env.CLAUDE_CODE_OAUTH_TOKEN && (
83
+ <Box marginTop={0} marginLeft={2}>
84
+ <Text color={theme.colors.text.muted} dimColor>Have Claude Pro/Max? Run aegis login --claude-pro to use your subscription</Text>
85
+ </Box>
86
+ )}
87
+ </Box>
88
+ );
89
+ });
90
+
91
+ WelcomeMessage.displayName = 'WelcomeMessage';
92
+
93
+ export default WelcomeMessage;
@@ -0,0 +1,7 @@
1
+ /**
2
+ *
3
+ */
4
+
5
+ export { ChatStatusBar } from './ChatStatusBar.js';
6
+ export { MessageArea, type UIMessage } from './MessageArea.js';
7
+ export { MessageList } from './MessageList.js';
@@ -0,0 +1,292 @@
1
+ /**
2
+ * CodeHighlighter - with line-level caching to avoid re-highlighting unchanged lines
3
+ * Supports diff rendering when language === 'diff'
4
+ */
5
+
6
+ import React, { useMemo, memo } from 'react';
7
+ import { Box, Text } from 'ink';
8
+ import { common, createLowlight } from 'lowlight';
9
+ import { themeManager } from '../../themes/index.js';
10
+ import type { SyntaxColors } from '../../themes/types.js';
11
+
12
+ const lowlight = createLowlight(common);
13
+
14
+ // Line-level cache: avoids re-highlighting the same line content
15
+ // Cache raw HAST nodes instead of React elements
16
+ const lineHighlightCache = new Map<string, any>();
17
+
18
+ function getCachedAst(
19
+ line: string,
20
+ language: string | undefined
21
+ ): any {
22
+ const cacheKey = `${language || 'auto'}|${line}`;
23
+ const cached = lineHighlightCache.get(cacheKey);
24
+ if (cached) return cached;
25
+
26
+ const ast = doHighlightAst(line, language);
27
+ if (lineHighlightCache.size > 2000) {
28
+ const firstKey = lineHighlightCache.keys().next().value;
29
+ if (firstKey) lineHighlightCache.delete(firstKey);
30
+ }
31
+ lineHighlightCache.set(cacheKey, ast);
32
+ return ast;
33
+ }
34
+
35
+ function doHighlightAst(
36
+ line: string,
37
+ language: string | undefined
38
+ ): any {
39
+ if (!line || line.trim() === '') {
40
+ return { type: 'root', children: [{ type: 'text', value: line || ' ' }] };
41
+ }
42
+ try {
43
+ if (language && lowlight.registered(language)) {
44
+ return lowlight.highlight(language, line);
45
+ }
46
+ if (/^[\s│┌┐└┘├┤┬┴┼─═║╔╗╚╝╠╣╦╩╬|+\-*=<>]+$/.test(line)) {
47
+ return { type: 'root', children: [{ type: 'text', value: line }] };
48
+ }
49
+ return lowlight.highlightAuto(line);
50
+ } catch {
51
+ return { type: 'root', children: [{ type: 'text', value: line }] };
52
+ }
53
+ }
54
+
55
+ // ── Diff rendering ──────────────────────────────────────────────────────────
56
+
57
+ // VS Code Dark+ exact hex colors
58
+ const VS_DIFF = {
59
+ add: '#3fb950', // additions
60
+ del: '#f85149', // deletions
61
+ hunk: '#79c0ff', // @@ hunk headers
62
+ header: '#e3b341', // --- +++ file headers
63
+ } as const;
64
+
65
+ const DIFF_HEADER_RE = /^(diff --git |index |--- |\+\+\+ )/;
66
+ const DIFF_HUNK_RE = /^@@ /;
67
+ const DIFF_ADD_RE = /^\+/;
68
+ const DIFF_DEL_RE = /^\-/;
69
+
70
+ function getDiffLineStyle(
71
+ line: string
72
+ ): { prefix: string; color: string; bold?: boolean } | null {
73
+ if (DIFF_HEADER_RE.test(line)) return { prefix: ' ', color: VS_DIFF.header, bold: true };
74
+ if (DIFF_HUNK_RE.test(line)) return { prefix: ' ', color: VS_DIFF.hunk };
75
+ if (DIFF_ADD_RE.test(line)) return { prefix: '+', color: VS_DIFF.add };
76
+ if (DIFF_DEL_RE.test(line)) return { prefix: '-', color: VS_DIFF.del };
77
+ return { prefix: ' ', color: '' };
78
+ }
79
+
80
+ interface CodeHighlighterProps {
81
+ content: string;
82
+ language?: string;
83
+ filePath?: string;
84
+ showLineNumbers?: boolean;
85
+ terminalWidth?: number;
86
+ startLine?: number;
87
+ }
88
+
89
+ export const CodeHighlighter: React.FC<CodeHighlighterProps> = ({
90
+ content,
91
+ language,
92
+ filePath,
93
+ showLineNumbers = true,
94
+ terminalWidth = 80,
95
+ startLine = 1,
96
+ }) => {
97
+ const theme = themeManager.getTheme();
98
+ const syntaxColors = theme.colors.syntax;
99
+
100
+ const lines = useMemo(() => content.split('\n'), [content]);
101
+
102
+ const totalLines = startLine + lines.length - 1;
103
+ const lineNumberWidth = showLineNumbers ? String(totalLines).length + 1 : 0;
104
+
105
+ const isDiff = language === 'diff' || language === 'patch';
106
+
107
+ return (
108
+ <Box
109
+ flexDirection="column"
110
+ paddingX={0}
111
+ marginY={0}
112
+ >
113
+ <Box>
114
+ <Box>
115
+ {filePath ? (
116
+ <>
117
+ <Text color={theme.colors.info}>{filePath}</Text>
118
+ {language && (
119
+ <Text color={theme.colors.text.muted} dimColor> {language}</Text>
120
+ )}
121
+ </>
122
+ ) : language ? (
123
+ <>
124
+ <Text color={theme.colors.text.muted} dimColor>{language}</Text>
125
+ </>
126
+ ) : null}
127
+ </Box>
128
+ <Box marginLeft={1}>
129
+ <Text color={theme.colors.text.muted} dimColor>/copy</Text>
130
+ </Box>
131
+ </Box>
132
+
133
+ {isDiff ? (
134
+ <DiffRenderer lines={lines} />
135
+ ) : (
136
+ lines.map((line, index) => {
137
+ const lineNumber = startLine + index;
138
+ return (
139
+ <Box key={index} flexDirection="row">
140
+ {showLineNumbers && (
141
+ <Box width={lineNumberWidth} marginRight={1}>
142
+ <Text dimColor>
143
+ {String(lineNumber).padStart(lineNumberWidth - 1, ' ')}
144
+ </Text>
145
+ </Box>
146
+ )}
147
+ <Box flexShrink={1}>
148
+ <CachedHighlightedLine line={line} language={language} syntaxColors={syntaxColors} />
149
+ </Box>
150
+ </Box>
151
+ );
152
+ })
153
+ )}
154
+ </Box>
155
+ );
156
+ };
157
+
158
+ /**
159
+ * Memoized per-line component using content-based cache
160
+ */
161
+ const CachedHighlightedLine: React.FC<{
162
+ line: string;
163
+ language?: string;
164
+ syntaxColors: SyntaxColors;
165
+ }> = memo(({ line, language, syntaxColors }) => {
166
+ const ast = useMemo(
167
+ () => getCachedAst(line, language),
168
+ [line, language]
169
+ );
170
+ const rendered = useMemo(
171
+ () => renderHastNode(ast, syntaxColors),
172
+ [ast, syntaxColors]
173
+ );
174
+ return <>{rendered}</>;
175
+ });
176
+ CachedHighlightedLine.displayName = 'CachedHighlightedLine';
177
+
178
+ function renderHastNode(
179
+ node: any,
180
+ syntaxColors: SyntaxColors,
181
+ key?: number
182
+ ): React.ReactNode {
183
+ if (node.type === 'text') {
184
+ return <Text key={key}>{node.value}</Text>;
185
+ }
186
+
187
+ if (node.type === 'root') {
188
+ return (
189
+ <>
190
+ {node.children?.map((child: any, index: number) =>
191
+ renderHastNode(child, syntaxColors, index)
192
+ )}
193
+ </>
194
+ );
195
+ }
196
+
197
+ if (node.type === 'element') {
198
+ const className = node.properties?.className?.[0] || '';
199
+ const color = getColorForClass(className, syntaxColors);
200
+
201
+ const children = node.children?.map((child: any, index: number) =>
202
+ renderHastNode(child, syntaxColors, index)
203
+ );
204
+
205
+ return (
206
+ <Text key={key} color={color}>
207
+ {children}
208
+ </Text>
209
+ );
210
+ }
211
+
212
+ return <Text key={key}></Text>;
213
+ }
214
+
215
+ function getColorForClass(className: string, syntaxColors: SyntaxColors): string {
216
+ if (className.includes('comment') || className.includes('prolog')) {
217
+ return syntaxColors.comment;
218
+ }
219
+ if (className.includes('string') || className.includes('char') || className.includes('template-string')) {
220
+ return syntaxColors.string;
221
+ }
222
+ if (className.includes('number') || className.includes('boolean') || className.includes('constant')) {
223
+ return syntaxColors.number;
224
+ }
225
+ if (className.includes('keyword') || className.includes('selector') || className.includes('important')) {
226
+ return syntaxColors.keyword;
227
+ }
228
+ if (className.includes('function') || className.includes('method')) {
229
+ return syntaxColors.function;
230
+ }
231
+ if (className.includes('variable') || className.includes('property')) {
232
+ return syntaxColors.variable;
233
+ }
234
+ if (className.includes('operator') || className.includes('punctuation')) {
235
+ return syntaxColors.operator;
236
+ }
237
+ if (className.includes('type') || className.includes('class-name') || className.includes('builtin')) {
238
+ return syntaxColors.type;
239
+ }
240
+ if (className.includes('tag') || className.includes('name')) {
241
+ return syntaxColors.tag;
242
+ }
243
+ if (className.includes('attr')) {
244
+ return syntaxColors.attr;
245
+ }
246
+ return syntaxColors.default;
247
+ }
248
+
249
+ // ── Diff Renderer ──────────────────────────────────────────────────────────
250
+
251
+ const DiffRenderer: React.FC<{ lines: string[] }> = memo(({ lines }) => {
252
+ return (
253
+ <>
254
+ {lines.map((line, index) => {
255
+ const style = getDiffLineStyle(line);
256
+ if (!style) {
257
+ // fallback: render as plain text
258
+ return (
259
+ <Box key={index} flexDirection="row">
260
+ <Text>{line}</Text>
261
+ </Box>
262
+ );
263
+ }
264
+ const { prefix: stylePrefix, color, bold } = style;
265
+ // Strip the first character (+/-) for display and show it as prefix instead
266
+ const displayText = color
267
+ ? (DIFF_ADD_RE.test(line) || DIFF_DEL_RE.test(line)
268
+ ? line.slice(1)
269
+ : line)
270
+ : line;
271
+
272
+ return (
273
+ <Box key={index} flexDirection="row">
274
+ <Box width={2} flexShrink={0}>
275
+ <Text bold={bold} color={color || undefined}>
276
+ {stylePrefix}
277
+ </Text>
278
+ </Box>
279
+ <Box flexShrink={1}>
280
+ <Text color={color || undefined} bold={bold}>
281
+ {displayText}
282
+ </Text>
283
+ </Box>
284
+ </Box>
285
+ );
286
+ })}
287
+ </>
288
+ );
289
+ });
290
+ DiffRenderer.displayName = 'DiffRenderer';
291
+
292
+ export default CodeHighlighter;