acmecode 1.0.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 (131) hide show
  1. package/.acmecode/config.json +6 -0
  2. package/README.md +124 -0
  3. package/dist/agent/index.js +161 -0
  4. package/dist/cli/bin/acmecode.js +3 -0
  5. package/dist/cli/package.json +25 -0
  6. package/dist/cli/src/index.d.ts +1 -0
  7. package/dist/cli/src/index.js +53 -0
  8. package/dist/config/index.js +92 -0
  9. package/dist/context/index.js +30 -0
  10. package/dist/core/src/agent/index.d.ts +52 -0
  11. package/dist/core/src/agent/index.js +476 -0
  12. package/dist/core/src/config/index.d.ts +83 -0
  13. package/dist/core/src/config/index.js +318 -0
  14. package/dist/core/src/context/index.d.ts +1 -0
  15. package/dist/core/src/context/index.js +30 -0
  16. package/dist/core/src/llm/provider.d.ts +27 -0
  17. package/dist/core/src/llm/provider.js +202 -0
  18. package/dist/core/src/llm/vision.d.ts +7 -0
  19. package/dist/core/src/llm/vision.js +37 -0
  20. package/dist/core/src/mcp/index.d.ts +10 -0
  21. package/dist/core/src/mcp/index.js +84 -0
  22. package/dist/core/src/prompt/anthropic.d.ts +1 -0
  23. package/dist/core/src/prompt/anthropic.js +32 -0
  24. package/dist/core/src/prompt/architect.d.ts +1 -0
  25. package/dist/core/src/prompt/architect.js +17 -0
  26. package/dist/core/src/prompt/autopilot.d.ts +1 -0
  27. package/dist/core/src/prompt/autopilot.js +18 -0
  28. package/dist/core/src/prompt/beast.d.ts +1 -0
  29. package/dist/core/src/prompt/beast.js +83 -0
  30. package/dist/core/src/prompt/gemini.d.ts +1 -0
  31. package/dist/core/src/prompt/gemini.js +45 -0
  32. package/dist/core/src/prompt/index.d.ts +18 -0
  33. package/dist/core/src/prompt/index.js +239 -0
  34. package/dist/core/src/prompt/zen.d.ts +1 -0
  35. package/dist/core/src/prompt/zen.js +13 -0
  36. package/dist/core/src/session/index.d.ts +18 -0
  37. package/dist/core/src/session/index.js +97 -0
  38. package/dist/core/src/skills/index.d.ts +6 -0
  39. package/dist/core/src/skills/index.js +72 -0
  40. package/dist/core/src/tools/batch.d.ts +2 -0
  41. package/dist/core/src/tools/batch.js +65 -0
  42. package/dist/core/src/tools/browser.d.ts +7 -0
  43. package/dist/core/src/tools/browser.js +86 -0
  44. package/dist/core/src/tools/edit.d.ts +11 -0
  45. package/dist/core/src/tools/edit.js +312 -0
  46. package/dist/core/src/tools/index.d.ts +13 -0
  47. package/dist/core/src/tools/index.js +980 -0
  48. package/dist/core/src/tools/lsp-client.d.ts +11 -0
  49. package/dist/core/src/tools/lsp-client.js +224 -0
  50. package/dist/index.js +41 -0
  51. package/dist/llm/provider.js +34 -0
  52. package/dist/mcp/index.js +84 -0
  53. package/dist/session/index.js +74 -0
  54. package/dist/skills/index.js +32 -0
  55. package/dist/tools/index.js +96 -0
  56. package/dist/tui/App.js +297 -0
  57. package/dist/tui/Spinner.js +16 -0
  58. package/dist/tui/TextInput.js +98 -0
  59. package/dist/tui/src/App.d.ts +11 -0
  60. package/dist/tui/src/App.js +1211 -0
  61. package/dist/tui/src/CatLogo.d.ts +10 -0
  62. package/dist/tui/src/CatLogo.js +99 -0
  63. package/dist/tui/src/OptionList.d.ts +15 -0
  64. package/dist/tui/src/OptionList.js +60 -0
  65. package/dist/tui/src/Spinner.d.ts +7 -0
  66. package/dist/tui/src/Spinner.js +18 -0
  67. package/dist/tui/src/TextInput.d.ts +28 -0
  68. package/dist/tui/src/TextInput.js +139 -0
  69. package/dist/tui/src/Tips.d.ts +2 -0
  70. package/dist/tui/src/Tips.js +62 -0
  71. package/dist/tui/src/Toast.d.ts +19 -0
  72. package/dist/tui/src/Toast.js +39 -0
  73. package/dist/tui/src/TodoItem.d.ts +7 -0
  74. package/dist/tui/src/TodoItem.js +21 -0
  75. package/dist/tui/src/i18n.d.ts +172 -0
  76. package/dist/tui/src/i18n.js +189 -0
  77. package/dist/tui/src/markdown.d.ts +6 -0
  78. package/dist/tui/src/markdown.js +356 -0
  79. package/dist/tui/src/theme.d.ts +31 -0
  80. package/dist/tui/src/theme.js +239 -0
  81. package/output.txt +0 -0
  82. package/package.json +44 -0
  83. package/packages/cli/package.json +25 -0
  84. package/packages/cli/src/index.ts +59 -0
  85. package/packages/cli/tsconfig.json +26 -0
  86. package/packages/core/package.json +39 -0
  87. package/packages/core/src/agent/index.ts +588 -0
  88. package/packages/core/src/config/index.ts +383 -0
  89. package/packages/core/src/context/index.ts +34 -0
  90. package/packages/core/src/llm/provider.ts +237 -0
  91. package/packages/core/src/llm/vision.ts +43 -0
  92. package/packages/core/src/mcp/index.ts +110 -0
  93. package/packages/core/src/prompt/anthropic.ts +32 -0
  94. package/packages/core/src/prompt/architect.ts +17 -0
  95. package/packages/core/src/prompt/autopilot.ts +18 -0
  96. package/packages/core/src/prompt/beast.ts +83 -0
  97. package/packages/core/src/prompt/gemini.ts +45 -0
  98. package/packages/core/src/prompt/index.ts +267 -0
  99. package/packages/core/src/prompt/zen.ts +13 -0
  100. package/packages/core/src/session/index.ts +129 -0
  101. package/packages/core/src/skills/index.ts +86 -0
  102. package/packages/core/src/tools/batch.ts +73 -0
  103. package/packages/core/src/tools/browser.ts +95 -0
  104. package/packages/core/src/tools/edit.ts +317 -0
  105. package/packages/core/src/tools/index.ts +1112 -0
  106. package/packages/core/src/tools/lsp-client.ts +303 -0
  107. package/packages/core/tsconfig.json +19 -0
  108. package/packages/tui/package.json +24 -0
  109. package/packages/tui/src/App.tsx +1702 -0
  110. package/packages/tui/src/CatLogo.tsx +134 -0
  111. package/packages/tui/src/OptionList.tsx +95 -0
  112. package/packages/tui/src/Spinner.tsx +28 -0
  113. package/packages/tui/src/TextInput.tsx +202 -0
  114. package/packages/tui/src/Tips.tsx +64 -0
  115. package/packages/tui/src/Toast.tsx +60 -0
  116. package/packages/tui/src/TodoItem.tsx +29 -0
  117. package/packages/tui/src/i18n.ts +203 -0
  118. package/packages/tui/src/markdown.ts +403 -0
  119. package/packages/tui/src/theme.ts +287 -0
  120. package/packages/tui/tsconfig.json +24 -0
  121. package/tsconfig.json +18 -0
  122. package/vscode-acmecode/.vscodeignore +11 -0
  123. package/vscode-acmecode/README.md +57 -0
  124. package/vscode-acmecode/esbuild.js +46 -0
  125. package/vscode-acmecode/images/button-dark.svg +5 -0
  126. package/vscode-acmecode/images/button-light.svg +5 -0
  127. package/vscode-acmecode/images/icon.png +1 -0
  128. package/vscode-acmecode/package-lock.json +490 -0
  129. package/vscode-acmecode/package.json +87 -0
  130. package/vscode-acmecode/src/extension.ts +128 -0
  131. package/vscode-acmecode/tsconfig.json +16 -0
@@ -0,0 +1,203 @@
1
+ import {
2
+ loadLangConfig,
3
+ saveGlobalLangConfig,
4
+ } from "@acmecode/core/config/index.js";
5
+
6
+ export type Language = "en" | "zh";
7
+
8
+ const translations = {
9
+ en: {
10
+ "command.exit": "Quit",
11
+ "command.clear": "Clear",
12
+ "command.model": "Models",
13
+ "command.skill": "Skills",
14
+ "command.theme": "Theme",
15
+ "command.lang": "Language",
16
+ "command.reason": "Reasoning",
17
+ "command.mode": "Mode",
18
+ "command.config": "Config",
19
+ "command.cancel": "Cancel Generation",
20
+ "status.thinking": "Thinking...",
21
+ "status.tool_complete": "Tool Complete",
22
+ "status.switched_model": "Switched to: {name}",
23
+ "status.switched_theme": "Switched to {name} theme",
24
+ "status.switched_lang": "Language switched to: {name}",
25
+ "status.skill_activated": "Activated skill: {name}",
26
+ "status.skill_not_found": "Skill not found: {name}",
27
+ "status.theme_not_found": "Theme not found: {name}",
28
+ "status.lang_not_found": "Language not found: {name}. Available: en, zh",
29
+ "status.config_saved": "Configuration for {name} saved.",
30
+ "status.no_skills": "No skills found",
31
+ "status.select_provider": "🌐 Select Provider:",
32
+ "status.available_providers": "🌐 Available Providers:",
33
+ "status.available_models": "📋 Available models:",
34
+ "status.available_themes": "🎨 Available themes:",
35
+ "status.available_langs": "🌐 Available languages:",
36
+ "status.available_reasoning": "🧠 Reasoning Levels:",
37
+ "status.available_modes": "⚙️ Agent Modes:",
38
+ "status.switched_reasoning": "Reasoning level set to: {name}",
39
+ "status.switched_mode": "Agent mode set to: {name}",
40
+ "status.context_usage": "Context:",
41
+ "status.reasoning_not_found": "Reasoning level not found: {name}",
42
+ "status.model_hint": "Use {cmd} to select.",
43
+ "status.theme_hint": "Use {cmd} to switch.",
44
+ "status.lang_hint": "Use {cmd} to switch.",
45
+ "status.reason_hint": "Use {cmd} to adjust thinking intensity.",
46
+ "status.mode_hint": "Use {cmd} to switch agent workflow.",
47
+ "status.current": "Current",
48
+ "ui.input_placeholder": "Ask AcmeCode (or type /exit)...",
49
+ "ui.tagline": "AI Coding Assistant",
50
+ "ui.tool_call": "Calling Tool",
51
+ "tool.read_file": "Read File",
52
+ "tool.write_file": "Write File",
53
+ "tool.run_command": "Run Command",
54
+ "tool.list_dir": "List Directory",
55
+ "tool.webfetch": "Fetch URL",
56
+ "tool.websearch": "Search Web",
57
+ "tool.codesearch": "Code Search",
58
+ "tool.grep_search": "Global Search",
59
+ "tool.switch_mode": "Switch Mode",
60
+ "tool.lsp": "Analyze Code",
61
+ "tool.edit_file": "Edit File",
62
+ "tool.batch": "Batch Execute",
63
+ "tool.browser_action": "Browser Action",
64
+ "mode.plan": "Plan (Architect)",
65
+ "mode.code": "Code (Execute Tasks)",
66
+ "mode.agent": "Agent (Autopilot)",
67
+ "mode.zen": "Zen (Minimalist)",
68
+ "status.mode": "Mode:",
69
+ "status.plan_file": "Plan:",
70
+ "tip.prefix": "Tip",
71
+ "ui.approval_required": "Approval Required",
72
+ "ui.dangerous_command_warning": "Dangerous Command Detected",
73
+ "ui.approve_execution_q": "Do you want to execute this command?",
74
+ "ui.approve": "Approve (Confirm)",
75
+ "ui.deny": "Deny (Reject)",
76
+ "ui.risk_level": "Risk Level: {level}",
77
+ "ui.risk_high": "HIGH",
78
+ "ui.risk_medium": "MEDIUM",
79
+ "ui.risk_low": "LOW",
80
+ "ui.local_guard_hit": "Local Safety Guard Triggered",
81
+ "ui.config_title": "⚙️ Configuration Wizard",
82
+ "ui.enter_api_key": "Enter API Key for {name}:",
83
+ "ui.enter_base_url": "Enter Base URL for {name} (e.g. {example}):",
84
+ "ui.select_protocol": "Select Interface Protocol for {name}:",
85
+ "ui.enter_custom_name": "Enter Custom Provider Name:",
86
+ "ui.add_custom_provider": "+ Add Custom Provider",
87
+ "ui.vision_model": "Vision Model",
88
+ "ui.disabled": "Disabled",
89
+ "ui.output_truncated": "Previous lines hidden for stability",
90
+ },
91
+ zh: {
92
+ "command.exit": "退出",
93
+ "command.clear": "清空",
94
+ "command.model": "模型",
95
+ "command.skill": "技能",
96
+ "command.theme": "主题",
97
+ "command.lang": "语言",
98
+ "command.reason": "推理",
99
+ "command.mode": "模式",
100
+ "command.config": "配置",
101
+ "command.cancel": "取消生成",
102
+ "status.thinking": "正在思考...",
103
+ "status.tool_complete": "工具完成",
104
+ "status.switched_model": "已切换到:{name}",
105
+ "status.switched_theme": "已切换到 {name} 主题",
106
+ "status.switched_lang": "语言设定已切换为:{name}",
107
+ "status.skill_activated": "已激活技能:{name}",
108
+ "status.skill_not_found": "未找到技能:{name}",
109
+ "status.theme_not_found": "未找到主题:{name}",
110
+ "status.lang_not_found": "未找到语言:{name}。可选:en, zh",
111
+ "status.config_saved": "{name} 的配置已保存。",
112
+ "status.no_skills": "未找到技能",
113
+ "status.select_provider": "🌐 选择服务商:",
114
+ "status.available_providers": "🌐 可用服务商:",
115
+ "status.available_models": "📋 可用模型:",
116
+ "status.available_themes": "🎨 可用主题:",
117
+ "status.available_langs": "🌐 可用语言:",
118
+ "status.available_reasoning": "🧠 推理级别:",
119
+ "status.available_modes": "⚙️ 工作模式:",
120
+ "status.switched_reasoning": "推理级别已设定为:{name}",
121
+ "status.switched_mode": "工作模式已设定为:{name}",
122
+ "status.context_usage": "上下文:",
123
+ "status.reasoning_not_found": "未找到推理级别:{name}",
124
+ "status.model_hint": "使用 {cmd} 进行选择。",
125
+ "status.theme_hint": "使用 {cmd} 进行切换。",
126
+ "status.lang_hint": "使用 {cmd} 进行切换。",
127
+ "status.reason_hint": "使用 {cmd} 调整思考强度。",
128
+ "status.mode_hint": "使用 {cmd} 切换工作流模式。",
129
+ "status.current": "当前",
130
+ "ui.input_placeholder": "问问 AcmeCode (输入 /exit 退出)...",
131
+ "ui.tagline": "AI 编码助手",
132
+ "ui.tool_call": "调用工具",
133
+ "tool.read_file": "读取文件",
134
+ "tool.write_file": "写入文件",
135
+ "tool.run_command": "执行命令",
136
+ "tool.list_dir": "列出目录",
137
+ "tool.webfetch": "获取网页",
138
+ "tool.websearch": "网页搜索",
139
+ "tool.codesearch": "代码搜索",
140
+ "tool.grep_search": "全局内容检索",
141
+ "tool.switch_mode": "切换模式",
142
+ "tool.lsp": "代码解析",
143
+ "tool.edit_file": "编辑文件",
144
+ "tool.batch": "批量执行",
145
+ "tool.browser_action": "浏览器操作",
146
+ "mode.plan": "Plan (方案建筑师)",
147
+ "mode.code": "Code (执行模式)",
148
+ "mode.agent": "Agent (无人驾驶)",
149
+ "mode.zen": "Zen (禅模式)",
150
+ "status.mode": "模式:",
151
+ "status.plan_file": "方案:",
152
+ "tip.prefix": "提示",
153
+ "ui.approval_required": "需要授权",
154
+ "ui.dangerous_command_warning": "检测到危险命令",
155
+ "ui.approve_execution_q": "是否确认执行该命令?",
156
+ "ui.approve": "允许执行",
157
+ "ui.deny": "拒绝执行",
158
+ "ui.risk_level": "风险等级:{level}",
159
+ "ui.risk_high": "高",
160
+ "ui.risk_medium": "中",
161
+ "ui.risk_low": "低",
162
+ "ui.local_guard_hit": "本地安全守卫触发",
163
+ "ui.config_title": "⚙️ 配置向导",
164
+ "ui.enter_api_key": "请输入 {name} 的 API Key:",
165
+ "ui.enter_base_url": "请输入 {name} 的 Base URL (例如 {example}):",
166
+ "ui.select_protocol": "请选择 {name} 的接口类型:",
167
+ "ui.enter_custom_name": "请输入自定义供应商名称:",
168
+ "ui.add_custom_provider": "+ 添加自定义供应商",
169
+ "ui.vision_model": "视觉模型",
170
+ "ui.disabled": "未开启",
171
+ "ui.output_truncated": "此处省略部分内容以保持输出稳定",
172
+ },
173
+ };
174
+
175
+ let currentLang: Language = (loadLangConfig() as Language) || "en";
176
+
177
+ export function t(
178
+ key: keyof typeof translations.en,
179
+ params: Record<string, string> = {},
180
+ ): string {
181
+ let text = translations[currentLang][key] || translations.en[key] || key;
182
+ for (const [k, v] of Object.entries(params)) {
183
+ text = text.replace(`{${k}}`, v);
184
+ }
185
+ return text;
186
+ }
187
+
188
+ export function setLang(lang: Language): boolean {
189
+ if (translations[lang]) {
190
+ currentLang = lang;
191
+ saveGlobalLangConfig(lang);
192
+ return true;
193
+ }
194
+ return false;
195
+ }
196
+
197
+ export function getLang(): Language {
198
+ return currentLang;
199
+ }
200
+
201
+ export function listLangs(): string[] {
202
+ return Object.keys(translations);
203
+ }
@@ -0,0 +1,403 @@
1
+ import { theme } from "./theme.js";
2
+ import pc from "picocolors"; // Keep pc for some low-level things if needed, but primarily use theme
3
+
4
+ /**
5
+ * Lightweight terminal markdown renderer.
6
+ * Converts common markdown syntax to ANSI-styled text for terminal display.
7
+ * Handles: headers, bold, italic, inline code, code blocks, lists, checkboxes, horizontal rules.
8
+ */
9
+ export function renderMarkdown(input: string): string {
10
+ const lines = input.split("\n");
11
+ const result: string[] = [];
12
+ let inCodeBlock = false;
13
+ let codeBlockLang = "";
14
+ let codeBuffer: string[] = [];
15
+
16
+ for (let i = 0; i < lines.length; i++) {
17
+ const line = lines[i]!;
18
+
19
+ // ── Code block toggle ──
20
+ if (line.trimStart().startsWith("```")) {
21
+ if (!inCodeBlock) {
22
+ inCodeBlock = true;
23
+ codeBlockLang = line.trimStart().slice(3).trim();
24
+ codeBuffer = [];
25
+ continue;
26
+ } else {
27
+ // End code block — render it
28
+ inCodeBlock = false;
29
+ if (codeBlockLang === "diff" || codeBlockLang === "patch") {
30
+ // Diff-specific rendering — colored lines, no box border
31
+ for (const codeLine of codeBuffer) {
32
+ result.push(renderDiffLine(codeLine));
33
+ }
34
+ } else {
35
+ const header = codeBlockLang
36
+ ? theme.muted(
37
+ `┌─ ${codeBlockLang} ${"─".repeat(Math.max(0, 36 - codeBlockLang.length))}`,
38
+ )
39
+ : theme.muted(`┌${"─".repeat(40)}`);
40
+ const footer = theme.muted(`└${"─".repeat(40)}`);
41
+ result.push(header);
42
+ for (const codeLine of codeBuffer) {
43
+ result.push(theme.muted("│ ") + highlightCode(codeLine, codeBlockLang));
44
+ }
45
+ result.push(footer);
46
+ }
47
+ codeBlockLang = "";
48
+ codeBuffer = [];
49
+ continue;
50
+ }
51
+ }
52
+
53
+ if (inCodeBlock) {
54
+ codeBuffer.push(line);
55
+ continue;
56
+ }
57
+
58
+ // ── Horizontal rule ──
59
+ if (/^(\s*[-*_]){3,}\s*$/.test(line)) {
60
+ result.push(theme.muted("─".repeat(40)));
61
+ continue;
62
+ }
63
+
64
+ // ── Headers ──
65
+ const h1Match = line.match(/^# (.+)/);
66
+ if (h1Match) {
67
+ result.push("");
68
+ result.push(theme.highlight(theme.primary(`█ ${h1Match[1]}`)));
69
+ result.push(
70
+ theme.muted("─".repeat(Math.min(40, (h1Match[1]?.length || 0) + 4))),
71
+ );
72
+ continue;
73
+ }
74
+
75
+ const h2Match = line.match(/^## (.+)/);
76
+ if (h2Match) {
77
+ result.push("");
78
+ result.push(theme.highlight(theme.info(`▌ ${h2Match[1]}`)));
79
+ continue;
80
+ }
81
+
82
+ const h3Match = line.match(/^### (.+)/);
83
+ if (h3Match) {
84
+ result.push(theme.highlight(theme.secondary(` ▸ ${h3Match[1]}`)));
85
+ continue;
86
+ }
87
+
88
+ const h4Match = line.match(/^#### (.+)/);
89
+ if (h4Match) {
90
+ result.push(theme.highlight(` ${h4Match[1]}`));
91
+ continue;
92
+ }
93
+
94
+ // ── Checkbox lists ──
95
+ const checkMatch = line.match(/^(\s*)- \[([ xX])\] (.+)/);
96
+ if (checkMatch) {
97
+ const indent = checkMatch[1] || "";
98
+ const checked = checkMatch[2]!.toLowerCase() === "x";
99
+ const text = formatInline(checkMatch[3]!);
100
+ const icon = checked ? theme.success("✔") : theme.muted("○");
101
+ result.push(
102
+ `${indent} ${icon} ${checked ? theme.muted(pc.strikethrough(text)) : text}`,
103
+ );
104
+ continue;
105
+ }
106
+
107
+ // ── Unordered lists ──
108
+ const ulMatch = line.match(/^(\s*)[-*] (.+)/);
109
+ if (ulMatch) {
110
+ const indent = ulMatch[1] || "";
111
+ const text = formatInline(ulMatch[2]!);
112
+ result.push(`${indent} ${theme.primary("•")} ${text}`);
113
+ continue;
114
+ }
115
+
116
+ // ── Ordered lists ──
117
+ const olMatch = line.match(/^(\s*)(\d+)\. (.+)/);
118
+ if (olMatch) {
119
+ const indent = olMatch[1] || "";
120
+ const num = olMatch[2]!;
121
+ const text = formatInline(olMatch[3]!);
122
+ result.push(`${indent} ${theme.primary(num + ".")} ${text}`);
123
+ continue;
124
+ }
125
+
126
+ // ── Blockquotes ──
127
+ const bqMatch = line.match(/^>\s?(.*)/);
128
+ if (bqMatch) {
129
+ result.push(` ${theme.muted("│")} ${theme.muted(pc.italic(bqMatch[1] || ""))}`);
130
+ continue;
131
+ }
132
+
133
+ // ── Regular text with inline formatting ──
134
+ result.push(formatInline(line));
135
+ }
136
+
137
+ // Handle unclosed code block
138
+ if (inCodeBlock && codeBuffer.length > 0) {
139
+ result.push(theme.muted(`┌${"─".repeat(40)}`));
140
+ for (const codeLine of codeBuffer) {
141
+ result.push(theme.muted("│ ") + highlightCode(codeLine, codeBlockLang));
142
+ }
143
+ result.push(theme.muted(`└${"─".repeat(40)}`));
144
+ }
145
+
146
+ return result.join("\n");
147
+ }
148
+
149
+ /**
150
+ * Format inline markdown: bold, italic, inline code, strikethrough, links
151
+ */
152
+ function formatInline(text: string): string {
153
+ // Inline code (must be first to prevent inner formatting)
154
+ text = text.replace(/`([^`]+)`/g, (_, code) =>
155
+ theme.warning(pc.bgBlack(` ${code} `)),
156
+ );
157
+
158
+ // Bold + italic
159
+ text = text.replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => theme.highlight(pc.italic(t)));
160
+
161
+ // Bold
162
+ text = text.replace(/\*\*(.+?)\*\*/g, (_, t) => theme.highlight(t));
163
+
164
+ // Italic
165
+ text = text.replace(/\*(.+?)\*/g, (_, t) => pc.italic(t));
166
+ text = text.replace(/_(.+?)_/g, (_, t) => pc.italic(t));
167
+
168
+ // Strikethrough
169
+ text = text.replace(/~~(.+?)~~/g, (_, t) => theme.muted(pc.strikethrough(t)));
170
+
171
+ // Links [text](url) → text (url)
172
+ text = text.replace(
173
+ /\[([^\]]+)\]\(([^)]+)\)/g,
174
+ (_, label, url) => `${pc.underline(theme.info(label))} ${theme.muted(`(${url})`)}`,
175
+ );
176
+
177
+ return text;
178
+ }
179
+
180
+ // ── Syntax highlighting keywords ──
181
+ const KEYWORDS = new Set([
182
+ // JS/TS
183
+ "const",
184
+ "let",
185
+ "var",
186
+ "function",
187
+ "return",
188
+ "if",
189
+ "else",
190
+ "for",
191
+ "while",
192
+ "do",
193
+ "switch",
194
+ "case",
195
+ "break",
196
+ "continue",
197
+ "new",
198
+ "this",
199
+ "class",
200
+ "extends",
201
+ "super",
202
+ "import",
203
+ "export",
204
+ "from",
205
+ "default",
206
+ "async",
207
+ "await",
208
+ "try",
209
+ "catch",
210
+ "finally",
211
+ "throw",
212
+ "typeof",
213
+ "instanceof",
214
+ "in",
215
+ "of",
216
+ "yield",
217
+ "delete",
218
+ "void",
219
+ // Python
220
+ "def",
221
+ "lambda",
222
+ "pass",
223
+ "raise",
224
+ "with",
225
+ "as",
226
+ "global",
227
+ "nonlocal",
228
+ "assert",
229
+ "elif",
230
+ "except",
231
+ "is",
232
+ "not",
233
+ "and",
234
+ "or",
235
+ "True",
236
+ "False",
237
+ "None",
238
+ "print",
239
+ // Common
240
+ "true",
241
+ "false",
242
+ "null",
243
+ "undefined",
244
+ "nil",
245
+ ]);
246
+
247
+ const TYPES = new Set([
248
+ "string",
249
+ "number",
250
+ "boolean",
251
+ "object",
252
+ "any",
253
+ "void",
254
+ "never",
255
+ "unknown",
256
+ "int",
257
+ "float",
258
+ "double",
259
+ "char",
260
+ "long",
261
+ "short",
262
+ "byte",
263
+ "bool",
264
+ "String",
265
+ "Number",
266
+ "Boolean",
267
+ "Array",
268
+ "Object",
269
+ "Map",
270
+ "Set",
271
+ "Promise",
272
+ "Record",
273
+ "Partial",
274
+ "Required",
275
+ "Readonly",
276
+ "interface",
277
+ "type",
278
+ "enum",
279
+ "struct",
280
+ "impl",
281
+ "trait",
282
+ "pub",
283
+ "fn",
284
+ "mod",
285
+ "use",
286
+ "crate",
287
+ ]);
288
+
289
+ /**
290
+ * Simple token-based syntax highlighter for code blocks.
291
+ */
292
+ function highlightCode(line: string, _lang: string): string {
293
+ // Line-level comment detection
294
+ const trimmed = line.trimStart();
295
+ if (
296
+ trimmed.startsWith("//") ||
297
+ trimmed.startsWith("#") ||
298
+ trimmed.startsWith("--")
299
+ ) {
300
+ return theme.muted(pc.italic(line));
301
+ }
302
+
303
+ let result = "";
304
+ let i = 0;
305
+
306
+ while (i < line.length) {
307
+ const ch = line[i]!;
308
+
309
+ // ── String literals ──
310
+ if (ch === '"' || ch === "'" || ch === "`") {
311
+ const quote = ch;
312
+ let end = i + 1;
313
+ while (end < line.length && line[end] !== quote) {
314
+ if (line[end] === "\\") end++; // skip escaped
315
+ end++;
316
+ }
317
+ end = Math.min(end + 1, line.length);
318
+ result += theme.success(line.slice(i, end));
319
+ i = end;
320
+ continue;
321
+ }
322
+
323
+ // ── Numbers ──
324
+ if (
325
+ /\d/.test(ch) &&
326
+ (i === 0 || /[\s(,=:+\-*/<>[\]{}!&|^~%]/.test(line[i - 1] || " "))
327
+ ) {
328
+ let end = i;
329
+ while (end < line.length && /[\d.xXa-fA-FeEoObB_]/.test(line[end]!))
330
+ end++;
331
+ result += theme.warning(line.slice(i, end));
332
+ i = end;
333
+ continue;
334
+ }
335
+
336
+ // ── Words (keywords/types/identifiers) ──
337
+ if (/[a-zA-Z_$@]/.test(ch)) {
338
+ let end = i;
339
+ while (end < line.length && /[a-zA-Z0-9_$]/.test(line[end]!)) end++;
340
+ const word = line.slice(i, end);
341
+
342
+ if (KEYWORDS.has(word)) {
343
+ result += theme.secondary(word);
344
+ } else if (TYPES.has(word)) {
345
+ result += theme.primary(word);
346
+ } else {
347
+ result += word;
348
+ }
349
+ i = end;
350
+ continue;
351
+ }
352
+
353
+ // ── Operators/punctuation ──
354
+ if (/[(){}[\];,.<>+\-*/%=!&|^~?:]/.test(ch)) {
355
+ result += theme.muted(ch);
356
+ i++;
357
+ continue;
358
+ }
359
+
360
+ result += ch;
361
+ i++;
362
+ }
363
+
364
+ return result;
365
+ }
366
+
367
+ // ANSI truecolor background helpers (terminals supporting 24-bit color)
368
+ function bgRgb(r: number, g: number, b: number, text: string): string {
369
+ return `\x1b[48;2;${r};${g};${b}m${text}\x1b[49m`;
370
+ }
371
+
372
+ // Diff background colors — matching opencode's diffAddedBg / diffRemovedBg
373
+ const DIFF_ADDED_BG = (t: string) => bgRgb(32, 48, 59, t); // #20303b deep teal-green
374
+ const DIFF_REMOVED_BG = (t: string) => bgRgb(55, 34, 44, t); // #37222c deep rose-red
375
+
376
+ /**
377
+ * Render a single line of a unified diff with appropriate colors.
378
+ * + added lines → green fg + deep green bg
379
+ * - removed lines → red fg + deep red bg
380
+ * @@ hunk headers → cyan
381
+ * --- / +++ file headers → bold dim
382
+ * context lines → dim
383
+ */
384
+ function renderDiffLine(line: string): string {
385
+ if (line.startsWith("+++") || line.startsWith("---")) {
386
+ return theme.muted(theme.highlight(line));
387
+ }
388
+ if (line.startsWith("@@")) {
389
+ const match = line.match(/^(@@ .+? @@)(.*)/);
390
+ if (match) {
391
+ return theme.info(match[1]!) + theme.muted(match[2] || "");
392
+ }
393
+ return theme.info(line);
394
+ }
395
+ if (line.startsWith("+")) {
396
+ return DIFF_ADDED_BG(theme.success(line));
397
+ }
398
+ if (line.startsWith("-")) {
399
+ return DIFF_REMOVED_BG(theme.danger(line));
400
+ }
401
+ // Context line
402
+ return theme.muted(line);
403
+ }