illusion-code 0.1.0__py3-none-any.whl

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 (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import {Box, Text} from 'ink';
3
+
4
+ import {useTheme} from '../theme/ThemeContext.js';
5
+
6
+ export function Footer({status, taskCount}: {status: Record<string, unknown>; taskCount: number}): React.JSX.Element {
7
+ const theme = useTheme();
8
+
9
+ return (
10
+ <Box marginTop={1} borderStyle="single" borderColor={theme.colors.muted} paddingX={1}>
11
+ <Text dimColor>
12
+ <Text color={theme.colors.primary}>model</Text>={String(status.model ?? 'unknown')}{' '}
13
+ <Text color={theme.colors.primary}>provider</Text>={String(status.provider ?? 'unknown')}{' '}
14
+ <Text color={theme.colors.primary}>auth</Text>={String(status.auth_status ?? 'unknown')}{' '}
15
+ <Text color={theme.colors.primary}>permission</Text>={String(status.permission_mode ?? 'unknown')}{' '}
16
+ <Text color={theme.colors.primary}>tasks</Text>={String(taskCount)}{' '}
17
+ <Text color={theme.colors.primary}>mcp</Text>={String(status.mcp_connected ?? 0)}/{String(status.mcp_failed ?? 0)}{' '}
18
+ <Text color={theme.colors.primary}>bridge</Text>={String(status.bridge_sessions ?? 0)}{' '}
19
+ <Text color={theme.colors.primary}>language</Text>={String(status.ui_language ?? 'zh-CN')}{' '}
20
+ <Text color={theme.colors.primary}>effort</Text>={String(status.effort ?? 'medium')}{' '}
21
+ <Text color={theme.colors.primary}>passes</Text>={String(status.passes ?? 1)}
22
+ </Text>
23
+ </Box>
24
+ );
25
+ }
@@ -0,0 +1,537 @@
1
+ import {lexer, Lexer} from 'marked';
2
+ import React, {type ReactNode, useMemo} from 'react';
3
+ import {Box, Text} from 'ink';
4
+ import type {Token, Tokens} from 'marked';
5
+ import {MarkdownTable} from './MarkdownTable.js';
6
+ import type {ThemeConfig} from '../theme/ThemeContext.js';
7
+ import {useTheme} from '../theme/ThemeContext.js';
8
+ import {useTerminalSize} from '../hooks/useTerminalSize.js';
9
+ import {stringWidth, padAligned, wrapText} from '../utils/markdown.js';
10
+
11
+ const INLINE_CODE_COLOR = '#b1b9f9';
12
+
13
+ const HTML_TAG_COLORS: Record<string, string | undefined> = {
14
+ kbd: '#56d4dd',
15
+ sub: undefined,
16
+ sup: undefined,
17
+ };
18
+
19
+ const HTML_TAG_RE = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/;
20
+
21
+ // 预处理 ^上标^ 语法:在 lexer 之前转换为 <sup> 标签
22
+ // 正则要求 ^ 前面不是字母/数字(避免 x^2 等数学表达式被误匹配),
23
+ // 且 ^ 后第一个字符不能是空白或 ^(避免 ^2 + y^ 等误匹配)
24
+ const _originalLex = Lexer.prototype.lex;
25
+ Lexer.prototype.lex = function (src: string) {
26
+ src = src.replace(/(?<![a-zA-Z0-9])\^([^\s^][^\^]*?)\^(?!\^)/g, '<sup>$1</sup>');
27
+ return _originalLex.call(this, src);
28
+ };
29
+
30
+ type MarkdownRenderStyle = {
31
+ color?: string;
32
+ italic?: boolean;
33
+ };
34
+
35
+ const NAMED_COLORS: Record<string, [number, number, number]> = {
36
+ black: [0, 0, 0], red: [205, 0, 0], green: [0, 205, 0], yellow: [205, 205, 0],
37
+ blue: [0, 0, 238], magenta: [205, 0, 205], cyan: [0, 205, 205], white: [229, 229, 229],
38
+ gray: [128, 128, 128], grey: [128, 128, 128],
39
+ };
40
+
41
+ function colorToAnsi(color: string): string {
42
+ if (color.startsWith('#')) {
43
+ const r = parseInt(color.slice(1, 3), 16);
44
+ const g = parseInt(color.slice(3, 5), 16);
45
+ const b = parseInt(color.slice(5, 7), 16);
46
+ return `38;2;${r};${g};${b}`;
47
+ }
48
+ const rgb = NAMED_COLORS[color.toLowerCase()];
49
+ if (rgb) return `38;2;${rgb[0]};${rgb[1]};${rgb[2]}`;
50
+ return '39';
51
+ }
52
+
53
+ function renderInline(
54
+ tokens: Token[] | undefined,
55
+ theme: ThemeConfig,
56
+ prefix: string,
57
+ style?: MarkdownRenderStyle,
58
+ ): ReactNode[] {
59
+ if (!tokens || tokens.length === 0) return [];
60
+ const result: ReactNode[] = [];
61
+
62
+ for (let i = 0; i < tokens.length; i++) {
63
+ const t = tokens[i];
64
+ const k = `${prefix}-${i}`;
65
+
66
+ switch (t.type) {
67
+ case 'strong': {
68
+ const st = t as Tokens.Strong;
69
+ result.push(
70
+ <Text key={k} bold color={style?.color} italic={style?.italic}>{renderInline(st.tokens, theme, k, style)}</Text>,
71
+ );
72
+ break;
73
+ }
74
+ case 'em': {
75
+ const et = t as Tokens.Em;
76
+ result.push(
77
+ <Text key={k} italic color={style?.color}>{renderInline(et.tokens, theme, k, style)}</Text>,
78
+ );
79
+ break;
80
+ }
81
+ case 'codespan': {
82
+ const ct = t as Tokens.Codespan;
83
+ result.push(
84
+ <Text key={k} color={style?.color ?? INLINE_CODE_COLOR} italic={style?.italic}>{ct.text}</Text>,
85
+ );
86
+ break;
87
+ }
88
+ case 'link': {
89
+ const lt = t as Tokens.Link;
90
+ result.push(
91
+ <Text key={k} color={style?.color ?? theme.colors.info} underline italic={style?.italic}>
92
+ {renderInline(lt.tokens, theme, k, style)}
93
+ </Text>,
94
+ );
95
+ break;
96
+ }
97
+ case 'text': {
98
+ const tt = t as Tokens.Text;
99
+ if (tt.tokens && tt.tokens.length > 0) {
100
+ result.push(...renderInline(tt.tokens, theme, k, style));
101
+ } else {
102
+ result.push(<Text key={k} color={style?.color} italic={style?.italic}>{tt.raw ?? tt.text}</Text>);
103
+ }
104
+ break;
105
+ }
106
+ case 'escape': {
107
+ result.push(<Text key={k} color={style?.color} italic={style?.italic}>{t.text}</Text>);
108
+ break;
109
+ }
110
+ case 'br': {
111
+ result.push(<Text key={k} color={style?.color} italic={style?.italic}>{'\n'}</Text>);
112
+ break;
113
+ }
114
+ case 'del': {
115
+ const dt = t as Tokens.Del;
116
+ result.push(
117
+ <Text key={k} strikethrough color={style?.color} italic={style?.italic}>{renderInline(dt.tokens, theme, k, style)}</Text>,
118
+ );
119
+ break;
120
+ }
121
+ case 'html': {
122
+ const raw = t.raw ?? (t as {text?: string}).text ?? '';
123
+ // 自闭合标签(<hr>、<br>)或无法识别的标签:跳过
124
+ if (/^<(hr|br|img)\b/i.test(raw)) break;
125
+ const m = raw.match(HTML_TAG_RE);
126
+ if (!m) break;
127
+ const isClosing = !!m[1];
128
+ if (isClosing) break; // 闭合标签,跳过
129
+ const tagName = m[2].toLowerCase();
130
+ // 开始标签:向后找到闭合标签,收集中间文本并施加样式
131
+ let innerText = '';
132
+ let found = false;
133
+ let j = i + 1;
134
+ for (; j < tokens.length; j++) {
135
+ const nt = tokens[j];
136
+ if (nt.type === 'html') {
137
+ const nRaw = nt.raw ?? (nt as {text?: string}).text ?? '';
138
+ const cm = nRaw.match(HTML_TAG_RE);
139
+ if (cm && cm[1] && cm[2].toLowerCase() === tagName) {
140
+ found = true;
141
+ break;
142
+ }
143
+ }
144
+ innerText += (nt as {text?: string}).text ?? (nt as {raw?: string}).raw ?? '';
145
+ }
146
+ if (found && innerText) {
147
+ const tagColor = HTML_TAG_COLORS[tagName];
148
+ result.push(
149
+ <Text key={k} color={tagColor ?? style?.color} bold={!!tagColor} italic={style?.italic}>
150
+ {innerText}
151
+ </Text>,
152
+ );
153
+ i = j; // 跳到闭合标签位置,循环 i++ 会移到下一个
154
+ }
155
+ break;
156
+ }
157
+ default: {
158
+ const raw = (t as {raw?: string}).raw ?? (t as {text?: string}).text ?? '';
159
+ result.push(<Text key={k} color={style?.color} italic={style?.italic}>{raw}</Text>);
160
+ break;
161
+ }
162
+ }
163
+ }
164
+
165
+ return result;
166
+ }
167
+
168
+ function renderItemContent(item: Tokens.ListItem, theme: ThemeConfig, prefix: string, style?: MarkdownRenderStyle): ReactNode {
169
+ if (!item.tokens || item.tokens.length === 0) {
170
+ return <Text color={style?.color} italic={style?.italic}>{item.text}</Text>;
171
+ }
172
+
173
+ const parts: ReactNode[] = [];
174
+ for (let i = 0; i < item.tokens.length; i++) {
175
+ const t = item.tokens[i];
176
+ const k = `${prefix}-${i}`;
177
+
178
+ if (t.type === 'text') {
179
+ const tt = t as Tokens.Text;
180
+ if (tt.tokens && tt.tokens.length > 0) {
181
+ parts.push(...renderInline(tt.tokens, theme, k, style));
182
+ } else {
183
+ parts.push(<Text key={k} color={style?.color} italic={style?.italic}>{tt.text}</Text>);
184
+ }
185
+ } else if (t.type === 'paragraph') {
186
+ const pt = t as Tokens.Paragraph;
187
+ parts.push(...renderInline(pt.tokens, theme, k, style));
188
+ } else if (t.type === 'list') {
189
+ // 嵌套列表:递归渲染,使用不同符号和缩进
190
+ const nestedList = t as Tokens.List;
191
+ const bullet = nestedList.ordered ? '' : `${theme.icons.bullet} `;
192
+ for (let ni = 0; ni < nestedList.items.length; ni++) {
193
+ const nestedItem = nestedList.items[ni];
194
+ const nestedContent = renderItemContent(nestedItem, theme, `${k}-${ni}`, style);
195
+ parts.push(
196
+ <Text key={`${k}-${ni}`} color={style?.color} italic={style?.italic}>{'\n'}{' '}<Text color={theme.colors.muted}>{bullet}</Text>{nestedContent}</Text>,
197
+ );
198
+ }
199
+ } else {
200
+ const raw = (t as {raw?: string}).raw ?? (t as {text?: string}).text ?? '';
201
+ parts.push(<Text key={k} color={style?.color} italic={style?.italic}>{raw}</Text>);
202
+ }
203
+ }
204
+ return <>{parts}</>;
205
+ }
206
+
207
+ function tokensToElements(
208
+ tokens: Token[],
209
+ theme: ThemeConfig,
210
+ terminalWidth: number,
211
+ style?: MarkdownRenderStyle,
212
+ ): ReactNode[] {
213
+ const elements: ReactNode[] = [];
214
+ let ki = 0;
215
+
216
+ for (const token of tokens) {
217
+ switch (token.type) {
218
+ case 'table': {
219
+ elements.push(
220
+ <MarkdownTable key={`t-${ki++}`} token={token as Tokens.Table} forceWidth={terminalWidth} />,
221
+ );
222
+ break;
223
+ }
224
+
225
+ case 'code': {
226
+ const ct = token as Tokens.Code;
227
+ const codeLines = ct.text.split('\n');
228
+ if (codeLines.length > 0 && codeLines[codeLines.length - 1] === '') {
229
+ codeLines.pop();
230
+ }
231
+ if (codeLines.length === 0) break;
232
+
233
+ const numWidth = String(codeLines.length).length;
234
+
235
+ // Border width: based on content, capped at terminal width
236
+ let maxContentWidth = 0;
237
+ for (const line of codeLines) {
238
+ const w = stringWidth(line || ' ');
239
+ if (w > maxContentWidth) maxContentWidth = w;
240
+ }
241
+ if (ct.lang) {
242
+ const lw = stringWidth(`${ct.lang}: ${codeLines.length} lines`);
243
+ if (lw > maxContentWidth) maxContentWidth = lw;
244
+ }
245
+ maxContentWidth = Math.max(maxContentWidth, 1);
246
+
247
+ // │ numStr │ code │ = numWidth + codeWidth + 7
248
+ const borderWidth = Math.min(numWidth + maxContentWidth + 7, terminalWidth - 4);
249
+ const codeWidth = borderWidth - numWidth - 7;
250
+ const lineDash = '─'.repeat(Math.max(borderWidth - 2, 0));
251
+
252
+ const innerLines: string[] = [];
253
+
254
+ // Language label + line count inside the border
255
+ if (ct.lang) {
256
+ const labelText = `${ct.lang}: ${codeLines.length} lines`;
257
+ const labelW = stringWidth(labelText);
258
+ const labelPad = ' '.repeat(Math.max(borderWidth - 3 - labelW, 0));
259
+ const gold = `\x1b[1m\x1b[${colorToAnsi(theme.colors.illusion)}m`;
260
+ const rst = '\x1b[39m\x1b[22m';
261
+ innerLines.push(`│ ${gold}${labelText}${rst}${labelPad}│`);
262
+ }
263
+
264
+ // Code lines: fully closed borders, wrap long lines only
265
+ if (codeWidth > 0) {
266
+ for (let li = 0; li < codeLines.length; li++) {
267
+ const line = codeLines[li] || ' ';
268
+ const lineNum = numWidth > 1
269
+ ? String(li + 1).padStart(numWidth, '0')
270
+ : String(li + 1);
271
+ const trimmed = line.trimStart();
272
+
273
+ let color = theme.colors.subtle;
274
+ if (trimmed.startsWith('+') && !trimmed.startsWith('+++')) {
275
+ color = theme.colors.success;
276
+ } else if (trimmed.startsWith('-') && !trimmed.startsWith('---')) {
277
+ color = theme.colors.error;
278
+ } else if (trimmed.startsWith('@@')) {
279
+ color = theme.colors.info;
280
+ }
281
+
282
+ const wrapped = wrapText(line, codeWidth, {hard: true});
283
+ for (let wi = 0; wi < wrapped.length; wi++) {
284
+ const segment = wrapped[wi]!;
285
+ const numStr = wi === 0 ? lineNum : ' '.repeat(numWidth);
286
+ const padded = padAligned(segment, stringWidth(segment), codeWidth, 'left');
287
+ const colored = `\x1b[${colorToAnsi(color)}m${padded}\x1b[39m`;
288
+ innerLines.push(`│ ${numStr} │ ${colored} │`);
289
+ }
290
+ }
291
+ }
292
+
293
+ const allLines = [`╭${lineDash}╮`, ...innerLines, `╰${lineDash}╯`];
294
+ elements.push(
295
+ <Text key={`t-${ki++}`} color={style?.color} italic={style?.italic}>{allLines.join('\n')}</Text>,
296
+ );
297
+ break;
298
+ }
299
+
300
+ case 'heading': {
301
+ const ht = token as Tokens.Heading;
302
+ const headingColor = ht.depth === 1
303
+ ? theme.colors.highlight
304
+ : ht.depth === 2
305
+ ? theme.colors.info
306
+ : theme.colors.illusionShimmer;
307
+ const headingStyle: MarkdownRenderStyle = {...style, color: headingColor};
308
+ const content = renderInline(ht.tokens, theme, `h-${ki}`, headingStyle);
309
+
310
+ if (ht.depth === 1) {
311
+ elements.push(
312
+ <Text key={`t-${ki++}`} bold underline color={headingColor} italic={style?.italic}>
313
+ {content}
314
+ </Text>,
315
+ );
316
+ } else if (ht.depth === 2) {
317
+ elements.push(
318
+ <Text key={`t-${ki++}`} bold color={headingColor} italic={style?.italic}>
319
+ {content}
320
+ </Text>,
321
+ );
322
+ } else {
323
+ elements.push(
324
+ <Text key={`t-${ki++}`} bold color={headingColor} italic={style?.italic}>
325
+ {content}
326
+ </Text>,
327
+ );
328
+ }
329
+ break;
330
+ }
331
+
332
+ case 'list': {
333
+ const lt = token as Tokens.List;
334
+ for (let li = 0; li < lt.items.length; li++) {
335
+ const item = lt.items[li];
336
+ const content = renderItemContent(item, theme, `l-${ki}-${li}`, style);
337
+ elements.push(
338
+ <Text key={`t-${ki++}`} color={style?.color} italic={style?.italic}>
339
+ <Text color={theme.colors.muted}>{`${theme.icons.arrow} `}</Text>
340
+ {content}
341
+ </Text>,
342
+ );
343
+ }
344
+ break;
345
+ }
346
+
347
+ case 'hr': {
348
+ elements.push(
349
+ <Text key={`t-${ki++}`} color={theme.colors.muted} italic={style?.italic}>{'─'.repeat(40)}</Text>,
350
+ );
351
+ break;
352
+ }
353
+
354
+ case 'blockquote': {
355
+ const bt = token as Tokens.Blockquote;
356
+ for (const inner of bt.tokens ?? []) {
357
+ if (inner.type === 'paragraph') {
358
+ const pt = inner as Tokens.Paragraph;
359
+ const content = renderInline(pt.tokens, theme, `bq-${ki}`, style);
360
+ elements.push(
361
+ <Text key={`t-${ki++}`} italic color={theme.colors.muted}>
362
+ {content}
363
+ </Text>,
364
+ );
365
+ } else if (inner.type === 'text') {
366
+ const tt = inner as Tokens.Text;
367
+ const content = renderInline(tt.tokens, theme, `bq-${ki}`, style);
368
+ elements.push(
369
+ <Text key={`t-${ki++}`} italic color={theme.colors.muted}>
370
+ {content}
371
+ </Text>,
372
+ );
373
+ }
374
+ }
375
+ break;
376
+ }
377
+
378
+ case 'paragraph': {
379
+ const pt = token as Tokens.Paragraph;
380
+ elements.push(
381
+ <Text key={`t-${ki++}`} color={style?.color} italic={style?.italic}>
382
+ {renderInline(pt.tokens, theme, `p-${ki}`, style)}
383
+ </Text>,
384
+ );
385
+ break;
386
+ }
387
+
388
+ case 'text': {
389
+ const tt = token as Tokens.Text;
390
+ if (tt.tokens && tt.tokens.length > 0) {
391
+ elements.push(
392
+ <Text key={`t-${ki++}`} color={style?.color} italic={style?.italic}>
393
+ {renderInline(tt.tokens, theme, `tx-${ki}`, style)}
394
+ </Text>,
395
+ );
396
+ } else {
397
+ const raw = tt.raw ?? tt.text ?? '';
398
+ raw.replace(/\n+$/, '').split('\n').forEach((line) => {
399
+ elements.push(<Text key={`t-${ki++}`} color={style?.color} italic={style?.italic}>{line}</Text>);
400
+ });
401
+ }
402
+ break;
403
+ }
404
+
405
+ case 'html': {
406
+ const ht = token as Tokens.HTML;
407
+ const raw = ht.raw ?? ht.text ?? '';
408
+ const lines = raw.replace(/\n+$/, '').split('\n');
409
+ let inDetails = false;
410
+ let inSummary = false;
411
+ let summaryText = '';
412
+ let detailLines: string[] = [];
413
+ const summaryTagRe = /<summary\b[^>]*>([\s\S]*?)<\/summary>/i;
414
+ const stripTags = (s: string) => s.replace(/<[^>]+>/g, '');
415
+ for (const line of lines) {
416
+ if (/^<details\b/i.test(line)) { inDetails = true; continue; }
417
+ if (/^<\/details>/i.test(line)) {
418
+ if (summaryText) {
419
+ elements.push(
420
+ <Text key={`t-${ki++}`} bold color={theme.colors.info}>{summaryText}</Text>,
421
+ );
422
+ }
423
+ for (const dl of detailLines) {
424
+ elements.push(
425
+ <Text key={`t-${ki++}`} color={style?.color}>{' '}{dl}</Text>,
426
+ );
427
+ }
428
+ inDetails = false; summaryText = ''; detailLines = [];
429
+ continue;
430
+ }
431
+ if (/<summary\b/i.test(line)) {
432
+ const sm = line.match(summaryTagRe);
433
+ if (sm) { summaryText = stripTags(sm[1]).trim(); }
434
+ inSummary = true;
435
+ if (!/<\/summary>/i.test(line)) continue;
436
+ }
437
+ if (/<\/summary>/i.test(line)) { inSummary = false; continue; }
438
+ if (inSummary) { summaryText += stripTags(line).trim(); continue; }
439
+ if (/^<hr\b/i.test(line)) {
440
+ elements.push(<Text key={`t-${ki++}`} color={theme.colors.muted}>{'─'.repeat(40)}</Text>);
441
+ continue;
442
+ }
443
+ const stripped = stripTags(line).trim();
444
+ if (!stripped) continue;
445
+ if (inDetails) { detailLines.push(stripped); continue; }
446
+ elements.push(<Text key={`t-${ki++}`} color={style?.color}>{stripped}</Text>);
447
+ }
448
+ break;
449
+ }
450
+
451
+ default: {
452
+ const raw = (token as {raw?: string}).raw;
453
+ if (raw) {
454
+ raw.replace(/\n+$/, '').split('\n').forEach((line) => {
455
+ elements.push(<Text key={`t-${ki++}`} color={style?.color} italic={style?.italic}>{line}</Text>);
456
+ });
457
+ }
458
+ break;
459
+ }
460
+ }
461
+ }
462
+
463
+ return elements;
464
+ }
465
+
466
+ export function renderInlineMarkdown(text: string, theme: ThemeConfig, keyPrefix: string, style?: MarkdownRenderStyle): ReactNode[] {
467
+ if (!text || !text.trim()) return [<Text key={`${keyPrefix}-empty`} color={style?.color} italic={style?.italic}>{text}</Text>];
468
+ try {
469
+ const tokens = lexer(text);
470
+ for (const token of tokens) {
471
+ if (token.type === 'paragraph') {
472
+ const pt = token as Tokens.Paragraph;
473
+ const rendered = renderInline(pt.tokens, theme, keyPrefix, style);
474
+ if (rendered.length > 0) return rendered;
475
+ } else if (token.type === 'text') {
476
+ const tt = token as Tokens.Text;
477
+ if (tt.tokens && tt.tokens.length > 0) {
478
+ const rendered = renderInline(tt.tokens, theme, keyPrefix, style);
479
+ if (rendered.length > 0) return rendered;
480
+ }
481
+ } else if (token.type === 'list') {
482
+ // 处理列表项:提取第一个列表项的内容
483
+ const lt = token as Tokens.List;
484
+ if (lt.items.length > 0) {
485
+ const item = lt.items[0];
486
+ if (item.tokens && item.tokens.length > 0) {
487
+ for (const itemToken of item.tokens) {
488
+ if (itemToken.type === 'text') {
489
+ const tt = itemToken as Tokens.Text;
490
+ if (tt.tokens && tt.tokens.length > 0) {
491
+ return [<Text key={`${keyPrefix}-list`} color={style?.color} italic={style?.italic}>{theme.icons.arrow} </Text>, ...renderInline(tt.tokens, theme, `${keyPrefix}-list`, style)];
492
+ }
493
+ } else if (itemToken.type === 'paragraph') {
494
+ const pt = itemToken as Tokens.Paragraph;
495
+ return [<Text key={`${keyPrefix}-list`} color={style?.color} italic={style?.italic}>{theme.icons.arrow} </Text>, ...renderInline(pt.tokens, theme, `${keyPrefix}-list`, style)];
496
+ }
497
+ }
498
+ }
499
+ // fallback: 用 raw 文本
500
+ return [<Text key={`${keyPrefix}-list`} color={style?.color} italic={style?.italic}>{theme.icons.arrow} {item.text}</Text>];
501
+ }
502
+ }
503
+ }
504
+ } catch {
505
+ // fall through to raw text
506
+ }
507
+ return [<Text key={`${keyPrefix}-raw`} color={style?.color} italic={style?.italic}>{text}</Text>];
508
+ }
509
+
510
+ export function MarkdownContent({
511
+ text,
512
+ style,
513
+ availableWidth,
514
+ }: {
515
+ text: string;
516
+ style?: MarkdownRenderStyle;
517
+ availableWidth?: number;
518
+ }): React.JSX.Element {
519
+ const theme = useTheme();
520
+ const {columns: terminalWidth} = useTerminalSize();
521
+ const contentWidth = Math.max(20, Math.min(availableWidth ?? terminalWidth, terminalWidth));
522
+ const elements = useMemo(() => {
523
+ if (!text.trim()) return [];
524
+ try {
525
+ const tokens = lexer(text);
526
+ return tokensToElements(tokens, theme, contentWidth, style);
527
+ } catch {
528
+ return text.split('\n').map((line, i) => <Text key={`f-${i}`} color={style?.color} italic={style?.italic}>{line}</Text>);
529
+ }
530
+ }, [text, theme, contentWidth, style]);
531
+
532
+ return (
533
+ <Box flexDirection="column" width={contentWidth}>
534
+ {elements}
535
+ </Box>
536
+ );
537
+ }