@pheem49/mint 1.5.5 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.codex +0 -0
  2. package/.github/FUNDING.yml +2 -0
  3. package/.github/workflows/ci.yml +45 -0
  4. package/.github/workflows/release.yml +79 -0
  5. package/Cargo.lock +5792 -0
  6. package/Cargo.toml +32 -0
  7. package/README.md +387 -353
  8. package/assets/icon.png +0 -0
  9. package/bin/mint +0 -0
  10. package/crates/mint-cli/Cargo.toml +23 -0
  11. package/crates/mint-cli/src/agent.rs +851 -0
  12. package/crates/mint-cli/src/gmail.rs +216 -0
  13. package/crates/mint-cli/src/image.rs +142 -0
  14. package/crates/mint-cli/src/main.rs +2837 -0
  15. package/crates/mint-cli/src/mcp.rs +63 -0
  16. package/crates/mint-cli/src/onboard.rs +1149 -0
  17. package/crates/mint-cli/src/setup.rs +390 -0
  18. package/crates/mint-cli/src/skills.rs +8 -0
  19. package/crates/mint-cli/src/updater.rs +279 -0
  20. package/crates/mint-core/Cargo.toml +22 -0
  21. package/crates/mint-core/src/agent_loop.rs +94 -0
  22. package/crates/mint-core/src/api_server.rs +991 -0
  23. package/crates/mint-core/src/channels.rs +248 -0
  24. package/crates/mint-core/src/chat.rs +895 -0
  25. package/crates/mint-core/src/code_tools.rs +729 -0
  26. package/crates/mint-core/src/config.rs +368 -0
  27. package/crates/mint-core/src/files.rs +159 -0
  28. package/crates/mint-core/src/knowledge.rs +541 -0
  29. package/crates/mint-core/src/lib.rs +84 -0
  30. package/crates/mint-core/src/mcp.rs +273 -0
  31. package/crates/mint-core/src/memory.rs +673 -0
  32. package/crates/mint-core/src/orchestration.rs +2157 -0
  33. package/crates/mint-core/src/pictures.rs +314 -0
  34. package/crates/mint-core/src/plugins.rs +727 -0
  35. package/crates/mint-core/src/safety.rs +416 -0
  36. package/crates/mint-core/src/semantic.rs +254 -0
  37. package/crates/mint-core/src/shell.rs +317 -0
  38. package/crates/mint-core/src/skills.rs +71 -0
  39. package/crates/mint-core/src/symbols.rs +157 -0
  40. package/crates/mint-core/src/tasks.rs +308 -0
  41. package/crates/mint-core/src/tts.rs +92 -0
  42. package/crates/mint-core/src/weather.rs +93 -0
  43. package/crates/mint-core/src/web_search.rs +200 -0
  44. package/crates/mint-core/src/workflows.rs +81 -0
  45. package/crates/mint-core/tests/mcp_stdio.rs +45 -0
  46. package/crates/mint-core/tests/memory_persistence.rs +172 -0
  47. package/crates/mint-core/tests/pictures_storage.rs +14 -0
  48. package/crates/mint-core/tests/task_lifecycle.rs +87 -0
  49. package/package.json +35 -99
  50. package/src/bin/index.js +16 -0
  51. package/src/renderer/index-web.html +17 -0
  52. package/src/renderer/index.html +17 -0
  53. package/src/renderer/public/Live2DCubismCore.js +9 -0
  54. package/src/renderer/public/assets/icon.png +0 -0
  55. package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
  56. package/src/renderer/src/App.tsx +33 -0
  57. package/src/renderer/src/calculator.ts +47 -0
  58. package/src/renderer/src/components/ChatPanel.tsx +1598 -0
  59. package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
  60. package/src/renderer/src/components/Live2DStage.tsx +374 -0
  61. package/src/renderer/src/components/MintDashboard.tsx +950 -0
  62. package/src/renderer/src/components/ModelPanel.tsx +154 -0
  63. package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
  64. package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
  65. package/src/renderer/src/components/ScreenPicker.tsx +579 -0
  66. package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
  67. package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
  68. package/src/renderer/src/components/WidgetWindow.tsx +36 -0
  69. package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
  70. package/src/{UI → renderer/src/css}/settings.css +69 -16
  71. package/src/renderer/src/css/spotlight.css +113 -0
  72. package/src/renderer/src/css/styles.css +3722 -0
  73. package/src/renderer/src/css/widget.css +185 -0
  74. package/src/renderer/src/env.d.ts +116 -0
  75. package/src/renderer/src/index.css +379 -0
  76. package/src/renderer/src/main.tsx +13 -0
  77. package/src/renderer/src/tauri.ts +996 -0
  78. package/src/renderer/src-web/App.tsx +25 -0
  79. package/src/renderer/src-web/calculator.ts +47 -0
  80. package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
  81. package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
  82. package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
  83. package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
  84. package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
  85. package/src/renderer/src-web/css/settings.css +1100 -0
  86. package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
  87. package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
  88. package/src/{UI → renderer/src-web/css}/widget.css +2 -2
  89. package/src/renderer/src-web/env.d.ts +107 -0
  90. package/src/renderer/src-web/index.css +379 -0
  91. package/src/renderer/src-web/main.tsx +13 -0
  92. package/src/renderer/src-web/tauri.ts +983 -0
  93. package/tsconfig.json +30 -0
  94. package/vite.config.ts +33 -0
  95. package/vite.config.web.ts +51 -0
  96. package/GUIDE_TH.md +0 -125
  97. package/assets/Agent_Mint.png +0 -0
  98. package/assets/CLI_Screen.png +0 -0
  99. package/assets/Settings.png +0 -0
  100. package/benchmark_ai.js +0 -71
  101. package/install.ps1 +0 -64
  102. package/install.sh +0 -54
  103. package/main.js +0 -139
  104. package/mint-cli-logic.js +0 -3
  105. package/mint-cli.js +0 -410
  106. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
  107. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +0 -23
  108. package/preload-picker.js +0 -11
  109. package/preload-settings.js +0 -11
  110. package/preload.js +0 -41
  111. package/scripts/install_linux_desktop_entry.js +0 -48
  112. package/src/AI_Brain/Gemini_API.js +0 -813
  113. package/src/AI_Brain/agent_orchestrator.js +0 -73
  114. package/src/AI_Brain/autonomous_brain.js +0 -179
  115. package/src/AI_Brain/behavior_memory.js +0 -135
  116. package/src/AI_Brain/headless_agent.js +0 -143
  117. package/src/AI_Brain/knowledge_base.js +0 -349
  118. package/src/AI_Brain/memory_store.js +0 -662
  119. package/src/AI_Brain/proactive_engine.js +0 -172
  120. package/src/AI_Brain/provider_adapter.js +0 -365
  121. package/src/Automation_Layer/browser_automation.js +0 -149
  122. package/src/Automation_Layer/file_operations.js +0 -286
  123. package/src/Automation_Layer/open_app.js +0 -85
  124. package/src/Automation_Layer/open_website.js +0 -38
  125. package/src/CLI/approval_handler.js +0 -47
  126. package/src/CLI/chat_router.js +0 -247
  127. package/src/CLI/chat_ui.js +0 -1159
  128. package/src/CLI/cli_colors.js +0 -115
  129. package/src/CLI/cli_formatters.js +0 -94
  130. package/src/CLI/code_agent.js +0 -1667
  131. package/src/CLI/code_session_memory.js +0 -62
  132. package/src/CLI/gmail_auth.js +0 -210
  133. package/src/CLI/image_input.js +0 -90
  134. package/src/CLI/intent_detectors.js +0 -181
  135. package/src/CLI/interactive_chat.js +0 -658
  136. package/src/CLI/list_features.js +0 -64
  137. package/src/CLI/onboarding.js +0 -416
  138. package/src/CLI/repo_summarizer.js +0 -282
  139. package/src/CLI/semantic_code_search.js +0 -312
  140. package/src/CLI/skill_manager.js +0 -41
  141. package/src/CLI/slash_command_handler.js +0 -418
  142. package/src/CLI/symbol_indexer.js +0 -231
  143. package/src/CLI/updater.js +0 -230
  144. package/src/CLI/workspace_manager.js +0 -90
  145. package/src/Channels/brave_search_bridge.js +0 -35
  146. package/src/Channels/discord_bridge.js +0 -66
  147. package/src/Channels/google_search_bridge.js +0 -38
  148. package/src/Channels/line_bridge.js +0 -60
  149. package/src/Channels/slack_bridge.js +0 -48
  150. package/src/Channels/telegram_bridge.js +0 -41
  151. package/src/Channels/whatsapp_bridge.js +0 -57
  152. package/src/Command_Parser/parser.js +0 -45
  153. package/src/Plugins/dev_tools.js +0 -41
  154. package/src/Plugins/discord.js +0 -20
  155. package/src/Plugins/docker.js +0 -47
  156. package/src/Plugins/gmail.js +0 -251
  157. package/src/Plugins/google_calendar.js +0 -252
  158. package/src/Plugins/mcp_manager.js +0 -95
  159. package/src/Plugins/notion.js +0 -256
  160. package/src/Plugins/obsidian.js +0 -54
  161. package/src/Plugins/plugin_manager.js +0 -81
  162. package/src/Plugins/spotify.js +0 -173
  163. package/src/Plugins/system_metrics.js +0 -31
  164. package/src/Plugins/system_monitor.js +0 -72
  165. package/src/System/action_executor.js +0 -178
  166. package/src/System/bridge_manager.js +0 -76
  167. package/src/System/chat_history_manager.js +0 -83
  168. package/src/System/config_manager.js +0 -194
  169. package/src/System/custom_workflows.js +0 -163
  170. package/src/System/daemon_manager.js +0 -67
  171. package/src/System/google_tts_urls.js +0 -51
  172. package/src/System/granular_automation.js +0 -157
  173. package/src/System/ipc_handlers.js +0 -332
  174. package/src/System/notifications.js +0 -23
  175. package/src/System/optional_require.js +0 -23
  176. package/src/System/picture_store.js +0 -109
  177. package/src/System/proactive_loop.js +0 -153
  178. package/src/System/safety_manager.js +0 -273
  179. package/src/System/sandbox_runner.js +0 -182
  180. package/src/System/screen_capture.js +0 -175
  181. package/src/System/smart_context.js +0 -227
  182. package/src/System/system_automation.js +0 -162
  183. package/src/System/system_events.js +0 -79
  184. package/src/System/system_info.js +0 -125
  185. package/src/System/task_manager.js +0 -222
  186. package/src/System/tool_registry.js +0 -293
  187. package/src/System/window_manager.js +0 -220
  188. package/src/UI/floating.css +0 -80
  189. package/src/UI/floating.html +0 -17
  190. package/src/UI/floating.js +0 -67
  191. package/src/UI/live2d_manager.js +0 -600
  192. package/src/UI/preload-floating.js +0 -7
  193. package/src/UI/preload-spotlight.js +0 -11
  194. package/src/UI/preload-widget.js +0 -5
  195. package/src/UI/proactive-glow.html +0 -42
  196. package/src/UI/renderer.js +0 -2127
  197. package/src/UI/screenPicker.html +0 -214
  198. package/src/UI/screenPicker.js +0 -262
  199. package/src/UI/settings.html +0 -577
  200. package/src/UI/settings.js +0 -770
  201. package/src/UI/spotlight.html +0 -23
  202. package/src/UI/spotlight.js +0 -185
  203. package/src/UI/widget.html +0 -29
  204. package/src/UI/widget.js +0 -10
  205. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  206. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
  207. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
  208. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
  209. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
  210. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
  211. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
  212. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
  213. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
  214. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
  215. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
  216. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
  217. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
  218. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
  219. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
  220. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
  221. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
  222. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
@@ -0,0 +1,851 @@
1
+ use std::io::{self, Write};
2
+ use std::path::Path;
3
+ use std::sync::{
4
+ Arc, Mutex,
5
+ atomic::{AtomicBool, Ordering},
6
+ };
7
+ use std::time::{Duration, Instant};
8
+
9
+ use anyhow::{Result, anyhow};
10
+ use mint_core::{
11
+ AgentApproval, AgentProgress, AgentResult, ApprovalOutcome, CHAT_CLI_ID, MintConfig,
12
+ OrchestrationError, orchestrate_agent_loop,
13
+ };
14
+
15
+ const RESET: &str = "\x1b[0m";
16
+ const MINT: &str = "\x1b[32m";
17
+ const BLUE: &str = "\x1b[38;2;78;201;216m";
18
+ const DIM: &str = "\x1b[90m";
19
+ const BRIGHT: &str = "\x1b[1;97m";
20
+
21
+ #[derive(Debug, Clone, Copy, Default)]
22
+ pub struct AgentOptions {
23
+ pub fast_mode: bool,
24
+ }
25
+
26
+ pub async fn run_code_agent(task: &str, root: &Path, config: &MintConfig) -> Result<AgentResult> {
27
+ run_code_agent_with_image(task, root, config, None).await
28
+ }
29
+
30
+ pub async fn run_code_agent_with_image(
31
+ task: &str,
32
+ root: &Path,
33
+ config: &MintConfig,
34
+ image_data_uri: Option<String>,
35
+ ) -> Result<AgentResult> {
36
+ run_code_agent_with_options(task, root, config, image_data_uri, AgentOptions::default()).await
37
+ }
38
+
39
+ pub async fn run_code_agent_with_options(
40
+ task: &str,
41
+ root: &Path,
42
+ config: &MintConfig,
43
+ image_data_uri: Option<String>,
44
+ options: AgentOptions,
45
+ ) -> Result<AgentResult> {
46
+ let started_at = Instant::now();
47
+ let approval_active = Arc::new(AtomicBool::new(false));
48
+ let agent_done = Arc::new(AtomicBool::new(false));
49
+ let live_status = Arc::new(Mutex::new(LiveStatus::default()));
50
+ let approve_approval_active = Arc::clone(&approval_active);
51
+ let approve_live_status = Arc::clone(&live_status);
52
+
53
+ let approve_cb = |approval: &AgentApproval| -> Result<ApprovalOutcome, String> {
54
+ approve_approval_active.store(true, Ordering::Relaxed);
55
+ if let Ok(mut status) = approve_live_status.lock() {
56
+ clear_live_status(&mut status);
57
+ }
58
+
59
+ struct ApprovalGuard(Arc<AtomicBool>);
60
+ impl Drop for ApprovalGuard {
61
+ fn drop(&mut self) {
62
+ self.0.store(false, Ordering::Relaxed);
63
+ }
64
+ }
65
+ let _guard = ApprovalGuard(Arc::clone(&approve_approval_active));
66
+
67
+ match approval {
68
+ AgentApproval::WriteFile { diff, .. } => {
69
+ println!(" Proposed edit");
70
+ print_colored_diff(diff);
71
+ if confirm_pausing_interrupt("Approve file edit? [y/N]", &approve_approval_active) {
72
+ Ok(ApprovalOutcome::Approved)
73
+ } else {
74
+ Ok(ApprovalOutcome::Denied)
75
+ }
76
+ }
77
+ AgentApproval::ApplyPatch { diff, .. } => {
78
+ println!(" Proposed edit");
79
+ print_colored_diff(diff);
80
+ if confirm_pausing_interrupt("Approve file edit? [y/N]", &approve_approval_active) {
81
+ Ok(ApprovalOutcome::Approved)
82
+ } else {
83
+ Ok(ApprovalOutcome::Denied)
84
+ }
85
+ }
86
+ AgentApproval::RunShell { command, mode } => {
87
+ println!(" {BLUE}• Proposed command{RESET}");
88
+ println!(" mode: {}", mode);
89
+ println!(" {}", command);
90
+ if confirm_pausing_interrupt(
91
+ "Approve local shell execution? [y/N]",
92
+ &approve_approval_active,
93
+ ) {
94
+ Ok(ApprovalOutcome::Approved)
95
+ } else {
96
+ Ok(ApprovalOutcome::Denied)
97
+ }
98
+ }
99
+ AgentApproval::NoteWrite { path, .. } => {
100
+ println!(" {BLUE}• Proposed note write{RESET}");
101
+ println!(" {}", path);
102
+ if confirm_pausing_interrupt(
103
+ "Approve writing this note? [y/N]",
104
+ &approve_approval_active,
105
+ ) {
106
+ Ok(ApprovalOutcome::Approved)
107
+ } else {
108
+ Ok(ApprovalOutcome::Denied)
109
+ }
110
+ }
111
+ AgentApproval::RunPlugin { name, instruction } => {
112
+ println!(" Run plugin {}: {}", name, instruction);
113
+ if confirm_pausing_interrupt(
114
+ &format!("Approve running plugin '{}'? [y/N]", name),
115
+ &approve_approval_active,
116
+ ) {
117
+ Ok(ApprovalOutcome::Approved)
118
+ } else {
119
+ Ok(ApprovalOutcome::Denied)
120
+ }
121
+ }
122
+ AgentApproval::McpTool { server, tool, .. } => {
123
+ println!(" Called MCP tool");
124
+ println!(" {} {}", server, tool);
125
+ if confirm_pausing_interrupt(
126
+ "Approve MCP tool call? [y/N]",
127
+ &approve_approval_active,
128
+ ) {
129
+ Ok(ApprovalOutcome::Approved)
130
+ } else {
131
+ Ok(ApprovalOutcome::Denied)
132
+ }
133
+ }
134
+ AgentApproval::UserApproval { title, prompt } => {
135
+ println!(" {BLUE}• Approval requested:{RESET} {}", title);
136
+ println!(" {}", prompt);
137
+ if confirm_pausing_interrupt(
138
+ "Approve this request? [y/N]",
139
+ &approve_approval_active,
140
+ ) {
141
+ Ok(ApprovalOutcome::Approved)
142
+ } else {
143
+ Ok(ApprovalOutcome::Denied)
144
+ }
145
+ }
146
+ AgentApproval::AskUser { question } => {
147
+ println!(" {BLUE}• Question from agent{RESET}");
148
+ println!(" {}", question);
149
+ print!("Answer (leave empty to decline): ");
150
+ let _ = std::io::Write::flush(&mut std::io::stdout());
151
+ let mut answer = String::new();
152
+ match std::io::stdin().read_line(&mut answer) {
153
+ Ok(_) if !answer.trim().is_empty() => {
154
+ Ok(ApprovalOutcome::Intercepted(answer.trim().to_owned()))
155
+ }
156
+ Ok(_) => Ok(ApprovalOutcome::Denied),
157
+ Err(error) => Err(error.to_string()),
158
+ }
159
+ }
160
+ }
161
+ };
162
+ let timer_live_status = Arc::clone(&live_status);
163
+ let timer_agent_done = Arc::clone(&agent_done);
164
+ let timer_approval_active = Arc::clone(&approval_active);
165
+ let timer_started_at = started_at;
166
+ if !options.fast_mode {
167
+ tokio::spawn(async move {
168
+ loop {
169
+ if timer_agent_done.load(Ordering::Relaxed) {
170
+ break;
171
+ }
172
+ if !timer_approval_active.load(Ordering::Relaxed)
173
+ && let Ok(mut status) = timer_live_status.lock()
174
+ {
175
+ status.thinking = Some(format!(
176
+ "Thinking ({} • Esc to interrupt)",
177
+ format_elapsed(timer_started_at.elapsed())
178
+ ));
179
+ render_live_status(&mut status);
180
+ }
181
+ tokio::time::sleep(Duration::from_secs(1)).await;
182
+ }
183
+ });
184
+ }
185
+ let progress_live_status = Arc::clone(&live_status);
186
+ let progress_cb = |progress: AgentProgress| match progress {
187
+ AgentProgress::Thinking { elapsed_secs } => {
188
+ if !options.fast_mode {
189
+ if let Ok(mut status) = progress_live_status.lock() {
190
+ status.thinking = Some(format!(
191
+ "Thinking ({} • Esc to interrupt)",
192
+ format_elapsed(Duration::from_secs(elapsed_secs))
193
+ ));
194
+ render_live_status(&mut status);
195
+ }
196
+ }
197
+ }
198
+ AgentProgress::Thought { thought } => {
199
+ if !options.fast_mode {
200
+ if let Ok(mut status) = progress_live_status.lock() {
201
+ commit_activity_snapshot(&mut status);
202
+ print_timeline_note(&thought);
203
+ status.thinking = None;
204
+ render_live_status(&mut status);
205
+ }
206
+ }
207
+ }
208
+ AgentProgress::ToolStart { action, input } => {
209
+ if !options.fast_mode {
210
+ if action == "create_plan" || action == "update_plan" {
211
+ if let Some(steps) = extract_plan_steps(&input) {
212
+ if let Ok(mut status) = progress_live_status.lock() {
213
+ status.thinking = None;
214
+ status.plan_steps = steps;
215
+ render_live_status(&mut status);
216
+ }
217
+ return;
218
+ }
219
+ }
220
+ if let Some(label) = explored_action_label(&action, &input) {
221
+ if let Ok(mut status) = progress_live_status.lock() {
222
+ status.thinking = None;
223
+ status.explored.push(label);
224
+ render_live_status(&mut status);
225
+ }
226
+ return;
227
+ }
228
+
229
+ let (is_activity, label) = match action.as_str() {
230
+ "web_search" => {
231
+ let query = input.get("query").and_then(|v| v.as_str()).unwrap_or("");
232
+ (
233
+ true,
234
+ format!("[web_search] Searching the web for \"{}\"...", query),
235
+ )
236
+ }
237
+ "run_shell" => {
238
+ let command = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
239
+ (
240
+ false,
241
+ format!("[run_shell] Running command: `{}`...", command),
242
+ )
243
+ }
244
+ "git_status" | "git_diff" | "git_log" | "git_branch" => {
245
+ (false, format!("[{}] Reading repository state...", action))
246
+ }
247
+ "create_plan" | "update_plan" => {
248
+ (false, format!("[{}] Updating task plan...", action))
249
+ }
250
+ "request_user_approval" => (
251
+ false,
252
+ "[request_user_approval] Waiting for approval...".into(),
253
+ ),
254
+ "ask_user" => (false, "[ask_user] Waiting for user answer...".into()),
255
+ "detect_project" => {
256
+ (false, "[detect_project] Detecting project type...".into())
257
+ }
258
+ "list_tests" => (false, "[list_tests] Listing tests...".into()),
259
+ "read_diagnostics" => {
260
+ (false, "[read_diagnostics] Reading diagnostics...".into())
261
+ }
262
+ "view_image" => {
263
+ let path = input.get("path").and_then(|v| v.as_str()).unwrap_or("");
264
+ (false, format!("[view_image] Reading image: {}...", path))
265
+ }
266
+ "write_file" => {
267
+ let path = input.get("path").and_then(|v| v.as_str()).unwrap_or("");
268
+ (false, format!("[write_file] Writing file: {}...", path))
269
+ }
270
+ "apply_patch" => {
271
+ let path = input
272
+ .get("patch")
273
+ .and_then(|p| p.get("path"))
274
+ .and_then(|v| v.as_str())
275
+ .unwrap_or("");
276
+ (false, format!("[apply_patch] Patching file: {}...", path))
277
+ }
278
+ "run_plugin" => {
279
+ let name = input.get("name").and_then(|v| v.as_str()).unwrap_or("");
280
+ (false, format!("[run_plugin] Running plugin: {}...", name))
281
+ }
282
+ "mcp_tool" => {
283
+ let tool_name = input.get("tool").and_then(|v| v.as_str()).unwrap_or("");
284
+ (
285
+ false,
286
+ format!("[mcp_tool] Running MCP tool: {}...", tool_name),
287
+ )
288
+ }
289
+ _ => (false, format!("[{}] Using tool...", action)),
290
+ };
291
+
292
+ if let Ok(mut status) = progress_live_status.lock() {
293
+ status.thinking = None;
294
+ if is_activity {
295
+ status.activities.push(label);
296
+ } else {
297
+ status.tasks.push(label);
298
+ }
299
+ render_live_status(&mut status);
300
+ }
301
+ }
302
+ }
303
+ AgentProgress::ToolEnd {
304
+ action,
305
+ input,
306
+ result,
307
+ } => {
308
+ if !options.fast_mode {
309
+ if action == "create_plan" || action == "update_plan" {
310
+ if let Some(steps) = extract_plan_steps(&input) {
311
+ if let Ok(mut status) = progress_live_status.lock() {
312
+ status.thinking = None;
313
+ status.plan_steps = steps;
314
+ render_live_status(&mut status);
315
+ }
316
+ }
317
+ } else if command_was_run(&result)
318
+ && let Some(commands) = ran_command_labels(&action, &input)
319
+ && let Ok(mut status) = progress_live_status.lock()
320
+ {
321
+ status.thinking = None;
322
+ for cmd in commands {
323
+ status.tasks.push(format!("Finished command: `{}`", cmd));
324
+ }
325
+ render_live_status(&mut status);
326
+ }
327
+ }
328
+ }
329
+ };
330
+
331
+ let chunk_live_status = Arc::clone(&live_status);
332
+ let on_chunk = |summary: String| {
333
+ if !options.fast_mode {
334
+ if let Ok(mut status) = chunk_live_status.lock() {
335
+ status.thinking = None;
336
+ commit_activity_snapshot(&mut status);
337
+ clear_live_status(&mut status);
338
+ }
339
+ }
340
+ let formatted_summary = format_markdown_bold(&summary);
341
+ print!("\n{MINT}Mint:{RESET} ");
342
+ render_live_summary(&formatted_summary);
343
+ println!();
344
+ };
345
+
346
+ let agent_loop = orchestrate_agent_loop(
347
+ config,
348
+ task,
349
+ root,
350
+ image_data_uri,
351
+ Some(CHAT_CLI_ID),
352
+ options.fast_mode,
353
+ approve_cb,
354
+ progress_cb,
355
+ on_chunk,
356
+ );
357
+ let res = if options.fast_mode {
358
+ agent_loop.await
359
+ } else {
360
+ tokio::select! {
361
+ res = agent_loop => res,
362
+ _ = wait_for_escape_interrupt(Arc::clone(&approval_active)) => {
363
+ Err(OrchestrationError::Agent("interrupted by Esc".into()))
364
+ }
365
+ }
366
+ };
367
+ agent_done.store(true, Ordering::Relaxed);
368
+ if !options.fast_mode {
369
+ if let Ok(mut status) = live_status.lock() {
370
+ status.thinking = None;
371
+ if res.is_err() {
372
+ commit_activity_snapshot(&mut status);
373
+ }
374
+ clear_live_status(&mut status);
375
+ }
376
+ }
377
+ let res = res.map_err(|e| anyhow!("{}", e))?;
378
+
379
+ if should_show_verification(&res.verification) {
380
+ println!("Verification: {}", res.verification);
381
+ }
382
+ println!(
383
+ "{DIM}─ Worked for {}{RESET}",
384
+ format_elapsed(started_at.elapsed())
385
+ );
386
+
387
+ let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
388
+ let width = tw as usize;
389
+ println!("{DIM}{}{RESET}", "─".repeat(width));
390
+
391
+ Ok(res)
392
+ }
393
+
394
+ fn clear_working_status() {
395
+ print!("\r\x1b[2K");
396
+ let _ = io::stdout().flush();
397
+ }
398
+
399
+ fn format_elapsed(duration: Duration) -> String {
400
+ let total_seconds = duration.as_secs();
401
+ let minutes = total_seconds / 60;
402
+ let seconds = total_seconds % 60;
403
+ if minutes == 0 {
404
+ format!("{seconds}s")
405
+ } else {
406
+ format!("{minutes}m {seconds:02}s")
407
+ }
408
+ }
409
+
410
+ fn render_live_summary(summary: &str) {
411
+ let mut chunk = String::new();
412
+ for character in summary.chars() {
413
+ chunk.push(character);
414
+ if chunk.chars().count() >= 96 {
415
+ print!("{chunk}");
416
+ let _ = io::stdout().flush();
417
+ chunk.clear();
418
+ }
419
+ }
420
+ if !chunk.is_empty() {
421
+ print!("{chunk}");
422
+ let _ = io::stdout().flush();
423
+ }
424
+ }
425
+
426
+ fn print_colored_diff(diff: &str) {
427
+ for line in diff.lines() {
428
+ if line.starts_with("@@") {
429
+ println!("{BLUE}{line}{RESET}");
430
+ } else if line.starts_with("--- ") || line.starts_with("+++ ") {
431
+ println!("{DIM}{line}{RESET}");
432
+ } else if line.starts_with('-') {
433
+ println!("\x1b[31m{line}\x1b[0m");
434
+ } else if line.starts_with('+') {
435
+ println!("\x1b[32m{line}\x1b[0m");
436
+ } else {
437
+ println!("{line}");
438
+ }
439
+ }
440
+ }
441
+
442
+ fn should_show_verification(verification: &str) -> bool {
443
+ let normalized = verification.trim().to_ascii_lowercase();
444
+ if normalized.is_empty() {
445
+ return false;
446
+ }
447
+ if normalized.starts_with("information retrieved from web search")
448
+ || normalized.starts_with("successfully ran background command")
449
+ || normalized.starts_with("opened ")
450
+ || normalized.contains("background command to open")
451
+ || normalized.contains("web search results")
452
+ {
453
+ return false;
454
+ }
455
+ !matches!(
456
+ normalized.as_str(),
457
+ "not run"
458
+ | "not run."
459
+ | "no checks run"
460
+ | "no checks run."
461
+ | "no technical task requested"
462
+ | "no technical task requested."
463
+ | "no technical task requested, just a greeting."
464
+ | "not required"
465
+ | "not required."
466
+ | "none"
467
+ | "n/a"
468
+ )
469
+ }
470
+
471
+ #[derive(Debug, Default)]
472
+ struct LiveStatus {
473
+ thinking: Option<String>,
474
+ explored: Vec<ExploredAction>,
475
+ activities: Vec<String>,
476
+ tasks: Vec<String>,
477
+ plan_steps: Vec<String>,
478
+ committed_explored: usize,
479
+ committed_activities: usize,
480
+ committed_tasks: usize,
481
+ rendered_lines: usize,
482
+ }
483
+
484
+ #[derive(Debug, Clone)]
485
+ struct ExploredAction {
486
+ kind: &'static str,
487
+ target: String,
488
+ }
489
+
490
+ fn explored_action_label(action: &str, input: &serde_json::Value) -> Option<ExploredAction> {
491
+ match action {
492
+ "list_files" => input
493
+ .get("path")
494
+ .and_then(|v| v.as_str())
495
+ .map(|path| ExploredAction {
496
+ kind: "[list_files] List",
497
+ target: display_tool_target(path),
498
+ }),
499
+ "read_file" => input
500
+ .get("path")
501
+ .and_then(|v| v.as_str())
502
+ .map(|path| ExploredAction {
503
+ kind: "[read_file] Read",
504
+ target: display_tool_target(path),
505
+ }),
506
+ "search_code" => {
507
+ let query = input.get("query").and_then(|v| v.as_str())?;
508
+ let path = input.get("path").and_then(|v| v.as_str()).unwrap_or(".");
509
+ let target = if path.trim().is_empty() || path == "." {
510
+ query.to_owned()
511
+ } else {
512
+ format!("{} in {}", query, display_tool_target(path))
513
+ };
514
+ Some(ExploredAction {
515
+ kind: "[search_code] Search",
516
+ target,
517
+ })
518
+ }
519
+ "symbols" => input
520
+ .get("path")
521
+ .and_then(|v| v.as_str())
522
+ .map(|path| ExploredAction {
523
+ kind: "[symbols] Index symbols",
524
+ target: display_tool_target(path),
525
+ }),
526
+ _ => None,
527
+ }
528
+ }
529
+
530
+ fn display_tool_target(path: &str) -> String {
531
+ if path.trim().is_empty() {
532
+ ".".into()
533
+ } else {
534
+ Path::new(path)
535
+ .file_name()
536
+ .and_then(|name| name.to_str())
537
+ .unwrap_or(path)
538
+ .into()
539
+ }
540
+ }
541
+
542
+ fn render_live_status(status: &mut LiveStatus) {
543
+ clear_live_status(status);
544
+ let mut lines = Vec::new();
545
+ let explored_start = status.committed_explored.min(status.explored.len());
546
+ let activities_start = status.committed_activities.min(status.activities.len());
547
+ let tasks_start = status.committed_tasks.min(status.tasks.len());
548
+
549
+ lines.extend(plan_lines(&status.plan_steps));
550
+ lines.extend(tasks_lines(&status.tasks[tasks_start..]));
551
+ lines.extend(activities_lines(&status.activities[activities_start..]));
552
+ lines.extend(explored_lines(&status.explored[explored_start..]));
553
+ if let Some(thinking) = &status.thinking {
554
+ lines.push(format!("{MINT}●{RESET} {BRIGHT}{thinking}{RESET}"));
555
+ }
556
+ if lines.is_empty() {
557
+ return;
558
+ }
559
+ for line in &lines {
560
+ println!("{line}");
561
+ }
562
+ status.rendered_lines = lines.len();
563
+ let _ = io::stdout().flush();
564
+ }
565
+
566
+ fn commit_activity_snapshot(status: &mut LiveStatus) {
567
+ clear_live_status(status);
568
+ let explored_start = status.committed_explored.min(status.explored.len());
569
+ let activities_start = status.committed_activities.min(status.activities.len());
570
+ let tasks_start = status.committed_tasks.min(status.tasks.len());
571
+
572
+ let mut lines = explored_lines(&status.explored[explored_start..]);
573
+ lines.extend(activities_lines(&status.activities[activities_start..]));
574
+ lines.extend(tasks_lines(&status.tasks[tasks_start..]));
575
+ if lines.is_empty() {
576
+ return;
577
+ }
578
+ for line in &lines {
579
+ println!("{line}");
580
+ }
581
+ print_timeline_separator();
582
+ status.committed_explored = status.explored.len();
583
+ status.committed_activities = status.activities.len();
584
+ status.committed_tasks = status.tasks.len();
585
+ let _ = io::stdout().flush();
586
+ }
587
+
588
+ fn print_timeline_note(thought: &str) {
589
+ let thought = thought.trim();
590
+ if thought.is_empty() {
591
+ return;
592
+ }
593
+ println!("{DIM}• {thought}{RESET}");
594
+ }
595
+
596
+ fn print_timeline_separator() {
597
+ let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
598
+ let width = term_width as usize;
599
+ println!("\n{DIM}{}{RESET}\n", "─".repeat(width));
600
+ }
601
+
602
+ fn clear_live_status(status: &mut LiveStatus) {
603
+ if status.rendered_lines == 0 {
604
+ clear_working_status();
605
+ return;
606
+ }
607
+ for _ in 0..status.rendered_lines {
608
+ print!("\x1b[1A\r\x1b[2K");
609
+ }
610
+ status.rendered_lines = 0;
611
+ let _ = io::stdout().flush();
612
+ }
613
+
614
+ fn explored_lines(actions: &[ExploredAction]) -> Vec<String> {
615
+ if actions.is_empty() {
616
+ return Vec::new();
617
+ }
618
+ let grouped = grouped_explored_actions(actions);
619
+ let mut lines = vec![format!("{BLUE}○ explored{RESET}")];
620
+ lines.extend(grouped.iter().take(24).enumerate().map(|(index, action)| {
621
+ let prefix = if index == 0 { " └" } else { " " };
622
+ format!("{DIM}{prefix} {action}{RESET}")
623
+ }));
624
+ if grouped.len() > 24 {
625
+ lines.push(format!("{DIM} ... {} more{RESET}", grouped.len() - 24));
626
+ }
627
+ lines
628
+ }
629
+
630
+ fn ran_command_labels(action: &str, input: &serde_json::Value) -> Option<Vec<String>> {
631
+ match action {
632
+ "run_shell" => input
633
+ .get("command")
634
+ .and_then(|v| v.as_str())
635
+ .filter(|command| !command.trim().is_empty())
636
+ .map(|command| vec![command.trim().to_owned()]),
637
+ "verify" => input
638
+ .get("commands")
639
+ .and_then(|v| v.as_array())
640
+ .map(|commands| {
641
+ commands
642
+ .iter()
643
+ .filter_map(|command| command.as_str())
644
+ .map(str::trim)
645
+ .filter(|command| !command.is_empty())
646
+ .map(str::to_owned)
647
+ .collect::<Vec<_>>()
648
+ }),
649
+ _ => None,
650
+ }
651
+ }
652
+
653
+ fn command_was_run(result: &str) -> bool {
654
+ result.lines().any(|line| line.starts_with("exit: "))
655
+ }
656
+
657
+ fn activities_lines(activities: &[String]) -> Vec<String> {
658
+ if activities.is_empty() {
659
+ return Vec::new();
660
+ }
661
+ let mut lines = vec![format!("{BLUE}◇ activity{RESET}")];
662
+ lines.extend(activities.iter().take(24).enumerate().map(|(index, act)| {
663
+ let prefix = if index == 0 { " └" } else { " " };
664
+ format!("{DIM}{prefix} {act}{RESET}")
665
+ }));
666
+ if activities.len() > 24 {
667
+ lines.push(format!("{DIM} ... {} more{RESET}", activities.len() - 24));
668
+ }
669
+ lines
670
+ }
671
+
672
+ fn extract_plan_steps(input: &serde_json::Value) -> Option<Vec<String>> {
673
+ let steps_val = input.get("steps")?;
674
+ let arr = steps_val.as_array()?;
675
+ let mut steps = Vec::new();
676
+ for v in arr {
677
+ if let Some(s) = v.as_str() {
678
+ steps.push(s.to_string());
679
+ }
680
+ }
681
+ Some(steps)
682
+ }
683
+
684
+ fn plan_lines(steps: &[String]) -> Vec<String> {
685
+ if steps.is_empty() {
686
+ return Vec::new();
687
+ }
688
+ let mut lines = vec![format!("{BLUE}● plan{RESET}")];
689
+ for (index, step) in steps.iter().enumerate() {
690
+ let prefix = if index == steps.len() - 1 {
691
+ " └"
692
+ } else {
693
+ " ├"
694
+ };
695
+ let (checked, text) = if step.to_lowercase().starts_with("done:") {
696
+ (format!("{MINT}[x]{RESET}"), step["done:".len()..].trim())
697
+ } else if step.to_lowercase().starts_with("done: ") {
698
+ (format!("{MINT}[x]{RESET}"), step["done: ".len()..].trim())
699
+ } else if step.to_lowercase().starts_with("in_progress:") {
700
+ (
701
+ format!("{BLUE}[~]{RESET}"),
702
+ step["in_progress:".len()..].trim(),
703
+ )
704
+ } else if step.to_lowercase().starts_with("in_progress: ") {
705
+ (
706
+ format!("{BLUE}[~]{RESET}"),
707
+ step["in_progress: ".len()..].trim(),
708
+ )
709
+ } else if step.to_lowercase().starts_with("todo:") {
710
+ (format!("{DIM}[ ]{RESET}"), step["todo:".len()..].trim())
711
+ } else if step.to_lowercase().starts_with("todo: ") {
712
+ (format!("{DIM}[ ]{RESET}"), step["todo: ".len()..].trim())
713
+ } else {
714
+ (format!("{DIM}[ ]{RESET}"), step.trim())
715
+ };
716
+ lines.push(format!("{DIM}{} {}{RESET} {}", prefix, checked, text));
717
+ }
718
+ lines
719
+ }
720
+
721
+ fn tasks_lines(tasks: &[String]) -> Vec<String> {
722
+ if tasks.is_empty() {
723
+ return Vec::new();
724
+ }
725
+ let mut lines = vec![format!("{BLUE}● tasks{RESET}")];
726
+ lines.extend(tasks.iter().take(24).enumerate().map(|(index, task)| {
727
+ let prefix = if index == 0 { " └" } else { " " };
728
+ format!("{DIM}{prefix} {task}{RESET}")
729
+ }));
730
+ if tasks.len() > 24 {
731
+ lines.push(format!("{DIM} ... {} more{RESET}", tasks.len() - 24));
732
+ }
733
+ lines
734
+ }
735
+
736
+ fn grouped_explored_actions(actions: &[ExploredAction]) -> Vec<String> {
737
+ let mut groups: Vec<(&str, Vec<&str>)> = Vec::new();
738
+ for action in actions {
739
+ if let Some((_, targets)) = groups.iter_mut().find(|(kind, _)| *kind == action.kind) {
740
+ if !targets.iter().any(|target| *target == action.target) {
741
+ targets.push(action.target.as_str());
742
+ }
743
+ } else {
744
+ groups.push((action.kind, vec![action.target.as_str()]));
745
+ }
746
+ }
747
+ groups
748
+ .into_iter()
749
+ .map(|(kind, targets)| format!("{} {}", kind, targets.join(", ")))
750
+ .collect()
751
+ }
752
+
753
+ async fn wait_for_escape_interrupt(approval_active: Arc<AtomicBool>) {
754
+ use crossterm::event::{self, Event, KeyCode};
755
+ use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
756
+
757
+ loop {
758
+ if approval_active.load(Ordering::Relaxed) {
759
+ tokio::time::sleep(Duration::from_millis(100)).await;
760
+ continue;
761
+ }
762
+
763
+ let _ = enable_raw_mode();
764
+ let escaped = matches!(event::poll(Duration::from_millis(0)), Ok(true))
765
+ && matches!(
766
+ event::read(),
767
+ Ok(Event::Key(key_event))
768
+ if key_event.kind == event::KeyEventKind::Press
769
+ && key_event.code == KeyCode::Esc
770
+ );
771
+ let _ = disable_raw_mode();
772
+
773
+ if escaped {
774
+ break;
775
+ }
776
+ tokio::time::sleep(Duration::from_millis(80)).await;
777
+ }
778
+ }
779
+
780
+ fn confirm_pausing_interrupt(prompt: &str, approval_active: &AtomicBool) -> bool {
781
+ approval_active.store(true, Ordering::Relaxed);
782
+ let approved = crate::confirm(prompt).unwrap_or(false);
783
+ approval_active.store(false, Ordering::Relaxed);
784
+ approved
785
+ }
786
+
787
+ fn format_markdown_bold(text: &str) -> String {
788
+ let mut formatted_lines = Vec::new();
789
+ let mut in_code_block = false;
790
+
791
+ for line in text.lines() {
792
+ let mut formatted_line = line.to_string();
793
+ let trimmed = line.trim_start();
794
+
795
+ if trimmed.starts_with("```") {
796
+ in_code_block = !in_code_block;
797
+ }
798
+
799
+ if !in_code_block {
800
+ let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
801
+ if hash_count > 0 && hash_count <= 6 {
802
+ let next_char = trimmed.chars().nth(hash_count);
803
+ let is_heading = match next_char {
804
+ Some(' ') => true,
805
+ Some(c) => !c.is_alphanumeric(),
806
+ None => true,
807
+ };
808
+ if is_heading {
809
+ // Make the entire heading line bold and bright white
810
+ formatted_line = format!("{}{}{}", BRIGHT, line, RESET);
811
+ } else {
812
+ formatted_line = process_inline_bold(&formatted_line);
813
+ }
814
+ } else {
815
+ formatted_line = process_inline_bold(&formatted_line);
816
+ }
817
+ } else {
818
+ // Do not format markdown inside code blocks
819
+ formatted_line = line.to_string();
820
+ }
821
+
822
+ formatted_lines.push(formatted_line);
823
+ }
824
+
825
+ let mut result = formatted_lines.join("\n");
826
+ if text.ends_with('\n') {
827
+ result.push('\n');
828
+ }
829
+ result
830
+ }
831
+
832
+ fn process_inline_bold(text: &str) -> String {
833
+ let count = text.matches("**").count();
834
+ let pair_limit = (count / 2) * 2;
835
+ let mut result = String::with_capacity(text.len());
836
+ let parts = text.split("**");
837
+ let mut is_bold = false;
838
+ let mut processed_markers = 0;
839
+ for part in parts {
840
+ if is_bold && processed_markers < pair_limit {
841
+ result.push_str(BLUE);
842
+ result.push_str(part);
843
+ result.push_str(RESET);
844
+ } else {
845
+ result.push_str(part);
846
+ }
847
+ processed_markers += 1;
848
+ is_bold = !is_bold;
849
+ }
850
+ result
851
+ }