@pheem49/mint 1.8.2 โ†’ 1.10.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 (106) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +297 -107
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +382 -127
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/image.rs +8 -8
  10. package/crates/mint-cli/src/interactive.rs +2888 -0
  11. package/crates/mint-cli/src/main.rs +391 -2506
  12. package/crates/mint-cli/src/markdown.rs +294 -0
  13. package/crates/mint-cli/src/onboard.rs +152 -109
  14. package/crates/mint-cli/src/setup.rs +143 -143
  15. package/crates/mint-cli/src/updater.rs +48 -6
  16. package/crates/mint-core/Cargo.toml +1 -0
  17. package/crates/mint-core/src/agent_loop.rs +1 -0
  18. package/crates/mint-core/src/api_server.rs +431 -65
  19. package/crates/mint-core/src/browser.rs +630 -102
  20. package/crates/mint-core/src/channels.rs +105 -6
  21. package/crates/mint-core/src/chat.rs +174 -11
  22. package/crates/mint-core/src/code_tools.rs +47 -31
  23. package/crates/mint-core/src/config.rs +302 -17
  24. package/crates/mint-core/src/image_gen.rs +299 -38
  25. package/crates/mint-core/src/lib.rs +10 -7
  26. package/crates/mint-core/src/mcp.rs +119 -6
  27. package/crates/mint-core/src/memory.rs +6 -1
  28. package/crates/mint-core/src/orchestration.rs +237 -96
  29. package/crates/mint-core/src/pictures.rs +67 -5
  30. package/crates/mint-core/src/semantic.rs +1 -1
  31. package/crates/mint-core/src/shell.rs +1 -1
  32. package/crates/mint-core/src/skills.rs +128 -25
  33. package/crates/mint-core/src/video_gen.rs +235 -0
  34. package/package.json +8 -2
  35. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  36. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  37. package/shared-modules/linux-audio/lilv.json +26 -19
  38. package/shared-modules/linux-audio/lv2.json +0 -1
  39. package/shared-modules/luajit/luajit.json +2 -2
  40. package/skills-lock.json +41 -0
  41. package/src/renderer/index-web.html +1 -1
  42. package/src/renderer/index.html +1 -1
  43. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  44. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  45. package/src/renderer/shared/platform.ts +3 -0
  46. package/src/renderer/shared/types.ts +4 -0
  47. package/src/renderer/shared/utils/approval.tsx +86 -7
  48. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  49. package/src/renderer/shared/utils/markdown.tsx +217 -88
  50. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  51. package/src/renderer/src/components/ChatPanel.tsx +486 -69
  52. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  53. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  54. package/src/renderer/src/components/MintDashboard.tsx +195 -7
  55. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  56. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  57. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  58. package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
  59. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  60. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  61. package/src/renderer/src/components/SettingsWindow.tsx +106 -3
  62. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  63. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  64. package/src/renderer/src/css/image-studio.css +106 -0
  65. package/src/renderer/src/css/settings.css +158 -0
  66. package/src/renderer/src/css/styles.css +368 -1
  67. package/src/renderer/src/css/veo-studio.css +626 -0
  68. package/src/renderer/src/index.css +1 -0
  69. package/src/renderer/src/tauri.ts +144 -17
  70. package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
  71. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  72. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  73. package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
  74. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  75. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  76. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  77. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
  78. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  79. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  80. package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
  81. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  82. package/src/renderer/src-web/css/image-studio.css +106 -0
  83. package/src/renderer/src-web/css/settings.css +158 -0
  84. package/src/renderer/src-web/css/styles.css +369 -1
  85. package/src/renderer/src-web/css/veo-studio.css +650 -0
  86. package/src/renderer/src-web/index.css +1 -0
  87. package/src/renderer/src-web/tauri.ts +135 -16
  88. package/src-tauri/src/desktop.rs +18 -16
  89. package/src-tauri/src/discord_rpc.rs +1 -1
  90. package/src-tauri/src/lib.rs +155 -25
  91. package/src-tauri/src/webhooks.rs +45 -31
  92. package/src-tauri/src/workflows.rs +1 -0
  93. package/src-tauri/tauri.conf.json +1 -1
  94. package/temp_gridgeist/AGENTS.md +8 -0
  95. package/temp_gridgeist/CHANGELOG.md +130 -0
  96. package/temp_gridgeist/LICENSE +21 -0
  97. package/temp_gridgeist/README.md +323 -0
  98. package/temp_gridgeist/README.th.md +325 -0
  99. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  100. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  101. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  102. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  103. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  104. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  105. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  106. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -14,6 +14,8 @@ use mint_core::{
14
14
 
15
15
  const RESET: &str = "\x1b[0m";
16
16
  const MINT: &str = "\x1b[32m";
17
+ const GREEN: &str = "\x1b[32m";
18
+ const RED: &str = "\x1b[31m";
17
19
  const BLUE: &str = "\x1b[38;2;78;201;216m";
18
20
  const DIM: &str = "\x1b[90m";
19
21
  const BRIGHT: &str = "\x1b[1;97m";
@@ -24,7 +26,7 @@ pub struct AgentOptions {
24
26
  }
25
27
 
26
28
  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
29
+ run_code_agent_with_image(task, root, config, None, None).await
28
30
  }
29
31
 
30
32
  pub async fn run_code_agent_with_image(
@@ -32,8 +34,17 @@ pub async fn run_code_agent_with_image(
32
34
  root: &Path,
33
35
  config: &MintConfig,
34
36
  image_data_uri: Option<String>,
37
+ video_data_uri: Option<String>,
35
38
  ) -> Result<AgentResult> {
36
- run_code_agent_with_options(task, root, config, image_data_uri, AgentOptions::default()).await
39
+ run_code_agent_with_options(
40
+ task,
41
+ root,
42
+ config,
43
+ image_data_uri,
44
+ video_data_uri,
45
+ AgentOptions::default(),
46
+ )
47
+ .await
37
48
  }
38
49
 
39
50
  pub async fn run_code_agent_with_options(
@@ -41,6 +52,7 @@ pub async fn run_code_agent_with_options(
41
52
  root: &Path,
42
53
  config: &MintConfig,
43
54
  image_data_uri: Option<String>,
55
+ video_data_uri: Option<String>,
44
56
  options: AgentOptions,
45
57
  ) -> Result<AgentResult> {
46
58
  let started_at = Instant::now();
@@ -65,30 +77,63 @@ pub async fn run_code_agent_with_options(
65
77
  let _guard = ApprovalGuard(Arc::clone(&approve_approval_active));
66
78
 
67
79
  match approval {
68
- AgentApproval::WriteFile { diff, .. } => {
69
- println!(" Proposed edit");
80
+ AgentApproval::WriteFile { path, diff, .. } => {
81
+ let (additions, deletions) = diff_stats(diff);
82
+ let colored_stats = format!(
83
+ "{DIM}({RESET}{GREEN}+{}{RESET} {RED}-{}{RESET}{DIM}){RESET}",
84
+ additions, deletions
85
+ );
86
+ let target_str = format!("{} {}", path, colored_stats);
87
+ print_approval_card(
88
+ "File Creation / Write",
89
+ &[("Target", &target_str), ("Action", "Write File")],
90
+ );
91
+ println!(
92
+ " {DIM}Proposed edit for {RESET}{BRIGHT}{}{RESET} {}:",
93
+ path, colored_stats
94
+ );
70
95
  print_colored_diff(diff);
71
- if confirm_pausing_interrupt("Approve file edit? [y/N]", &approve_approval_active) {
96
+ if confirm_pausing_interrupt(
97
+ &format!("Approve writing file '{}'?", path),
98
+ &approve_approval_active,
99
+ ) {
72
100
  Ok(ApprovalOutcome::Approved)
73
101
  } else {
74
102
  Ok(ApprovalOutcome::Denied)
75
103
  }
76
104
  }
77
- AgentApproval::ApplyPatch { diff, .. } => {
78
- println!(" Proposed edit");
105
+ AgentApproval::ApplyPatch { path, diff, .. } => {
106
+ let (additions, deletions) = diff_stats(diff);
107
+ let colored_stats = format!(
108
+ "{DIM}({RESET}{GREEN}+{}{RESET} {RED}-{}{RESET}{DIM}){RESET}",
109
+ additions, deletions
110
+ );
111
+ let target_str = format!("{} {}", path, colored_stats);
112
+ print_approval_card(
113
+ "File Modification",
114
+ &[("Target", &target_str), ("Action", "Apply Patch")],
115
+ );
116
+ println!(
117
+ " {DIM}Proposed edit for {RESET}{BRIGHT}{}{RESET} {}:",
118
+ path, colored_stats
119
+ );
79
120
  print_colored_diff(diff);
80
- if confirm_pausing_interrupt("Approve file edit? [y/N]", &approve_approval_active) {
121
+ if confirm_pausing_interrupt(
122
+ &format!("Approve patching file '{}'?", path),
123
+ &approve_approval_active,
124
+ ) {
81
125
  Ok(ApprovalOutcome::Approved)
82
126
  } else {
83
127
  Ok(ApprovalOutcome::Denied)
84
128
  }
85
129
  }
86
130
  AgentApproval::RunShell { command, mode } => {
87
- println!(" {BLUE}โ€ข Proposed command{RESET}");
88
- println!(" mode: {}", mode);
89
- println!(" {}", command);
131
+ print_approval_card(
132
+ "Local Shell Command",
133
+ &[("Command", command), ("Mode", mode)],
134
+ );
90
135
  if confirm_pausing_interrupt(
91
- "Approve local shell execution? [y/N]",
136
+ "Approve running shell command?",
92
137
  &approve_approval_active,
93
138
  ) {
94
139
  Ok(ApprovalOutcome::Approved)
@@ -97,21 +142,21 @@ pub async fn run_code_agent_with_options(
97
142
  }
98
143
  }
99
144
  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
- ) {
145
+ print_approval_card("Note Creation", &[("Path", path)]);
146
+ if confirm_pausing_interrupt("Approve writing this note?", &approve_approval_active)
147
+ {
106
148
  Ok(ApprovalOutcome::Approved)
107
149
  } else {
108
150
  Ok(ApprovalOutcome::Denied)
109
151
  }
110
152
  }
111
153
  AgentApproval::RunPlugin { name, instruction } => {
112
- println!(" Run plugin {}: {}", name, instruction);
154
+ print_approval_card(
155
+ "Plugin Execution",
156
+ &[("Plugin", name), ("Detail", instruction)],
157
+ );
113
158
  if confirm_pausing_interrupt(
114
- &format!("Approve running plugin '{}'? [y/N]", name),
159
+ &format!("Approve running plugin '{}'?", name),
115
160
  &approve_approval_active,
116
161
  ) {
117
162
  Ok(ApprovalOutcome::Approved)
@@ -119,34 +164,38 @@ pub async fn run_code_agent_with_options(
119
164
  Ok(ApprovalOutcome::Denied)
120
165
  }
121
166
  }
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
- ) {
167
+ AgentApproval::McpTool {
168
+ server,
169
+ tool,
170
+ arguments,
171
+ } => {
172
+ let mut fields = vec![("Server", server.as_str()), ("Tool", tool.as_str())];
173
+ let formatted_args = arguments.to_string();
174
+ if !formatted_args.is_empty() && formatted_args != "{}" && formatted_args != "null"
175
+ {
176
+ fields.push(("Arguments", &formatted_args));
177
+ }
178
+ print_approval_card("MCP Tool Call", &fields);
179
+ if confirm_pausing_interrupt("Approve MCP tool call?", &approve_approval_active) {
129
180
  Ok(ApprovalOutcome::Approved)
130
181
  } else {
131
182
  Ok(ApprovalOutcome::Denied)
132
183
  }
133
184
  }
134
185
  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
- ) {
186
+ print_approval_card(
187
+ "Security Authorization",
188
+ &[("Title", title), ("Detail", prompt)],
189
+ );
190
+ if confirm_pausing_interrupt("Approve this request?", &approve_approval_active) {
141
191
  Ok(ApprovalOutcome::Approved)
142
192
  } else {
143
193
  Ok(ApprovalOutcome::Denied)
144
194
  }
145
195
  }
146
196
  AgentApproval::AskUser { question } => {
147
- println!(" {BLUE}โ€ข Question from agent{RESET}");
148
- println!(" {}", question);
149
- print!("Answer (leave empty to decline): ");
197
+ print_approval_card("Agent Question", &[("Question", question)]);
198
+ print!(" Answer (leave empty to decline): ");
150
199
  let _ = std::io::Write::flush(&mut std::io::stdout());
151
200
  let mut answer = String::new();
152
201
  match std::io::stdin().read_line(&mut answer) {
@@ -189,39 +238,47 @@ pub async fn run_code_agent_with_options(
189
238
  agent_name,
190
239
  model_name,
191
240
  } => {
192
- if !options.fast_mode {
193
- if let Ok(mut status) = progress_live_status.lock() {
194
- let label = if let (Some(a), Some(m)) = (agent_name, model_name) {
195
- format!("{} ({}) is thinking ({} โ€ข Esc to interrupt)", a, m, format_elapsed(Duration::from_secs(elapsed_secs)))
196
- } else {
197
- format!("Thinking ({} โ€ข Esc to interrupt)", format_elapsed(Duration::from_secs(elapsed_secs)))
198
- };
199
- status.thinking = Some(label);
200
- render_live_status(&mut status);
201
- }
241
+ if !options.fast_mode
242
+ && let Ok(mut status) = progress_live_status.lock()
243
+ {
244
+ let label = if let (Some(a), Some(m)) = (agent_name, model_name) {
245
+ format!(
246
+ "{} ({}) is thinking ({} โ€ข Esc to interrupt)",
247
+ a,
248
+ m,
249
+ format_elapsed(Duration::from_secs(elapsed_secs))
250
+ )
251
+ } else {
252
+ format!(
253
+ "Thinking ({} โ€ข Esc to interrupt)",
254
+ format_elapsed(Duration::from_secs(elapsed_secs))
255
+ )
256
+ };
257
+ status.thinking = Some(label);
258
+ render_live_status(&mut status);
202
259
  }
203
260
  }
204
261
  AgentProgress::Thought { thought } => {
205
- if !options.fast_mode {
206
- if let Ok(mut status) = progress_live_status.lock() {
207
- commit_activity_snapshot(&mut status);
208
- print_timeline_note(&thought);
209
- status.thinking = None;
210
- render_live_status(&mut status);
211
- }
262
+ if !options.fast_mode
263
+ && let Ok(mut status) = progress_live_status.lock()
264
+ {
265
+ commit_activity_snapshot(&mut status);
266
+ print_timeline_note(&thought);
267
+ status.thinking = None;
268
+ render_live_status(&mut status);
212
269
  }
213
270
  }
214
271
  AgentProgress::ToolStart { action, input } => {
215
272
  if !options.fast_mode {
216
- if action == "create_plan" || action == "update_plan" {
217
- if let Some(steps) = extract_plan_steps(&input) {
218
- if let Ok(mut status) = progress_live_status.lock() {
219
- status.thinking = None;
220
- status.plan_steps = steps;
221
- render_live_status(&mut status);
222
- }
223
- return;
273
+ if (action == "create_plan" || action == "update_plan")
274
+ && let Some(steps) = extract_plan_steps(&input)
275
+ {
276
+ if let Ok(mut status) = progress_live_status.lock() {
277
+ status.thinking = None;
278
+ status.plan_steps = steps;
279
+ render_live_status(&mut status);
224
280
  }
281
+ return;
225
282
  }
226
283
  if let Some(label) = explored_action_label(&action, &input) {
227
284
  if let Ok(mut status) = progress_live_status.lock() {
@@ -313,12 +370,12 @@ pub async fn run_code_agent_with_options(
313
370
  } => {
314
371
  if !options.fast_mode {
315
372
  if action == "create_plan" || action == "update_plan" {
316
- if let Some(steps) = extract_plan_steps(&input) {
317
- if let Ok(mut status) = progress_live_status.lock() {
318
- status.thinking = None;
319
- status.plan_steps = steps;
320
- render_live_status(&mut status);
321
- }
373
+ if let Some(steps) = extract_plan_steps(&input)
374
+ && let Ok(mut status) = progress_live_status.lock()
375
+ {
376
+ status.thinking = None;
377
+ status.plan_steps = steps;
378
+ render_live_status(&mut status);
322
379
  }
323
380
  } else if command_was_run(&result)
324
381
  && let Some(commands) = ran_command_labels(&action, &input)
@@ -335,39 +392,38 @@ pub async fn run_code_agent_with_options(
335
392
  if action == "web_search"
336
393
  && !result.starts_with("Web search error:")
337
394
  && result != "No web search results found."
395
+ && let Ok(mut status) = progress_live_status.lock()
338
396
  {
339
- if let Ok(mut status) = progress_live_status.lock() {
340
- let sources = parse_web_search_sources(&result);
341
- status.web_sources.extend(sources);
342
- }
397
+ let sources = parse_web_search_sources(&result);
398
+ status.web_sources.extend(sources);
343
399
  }
344
400
  }
345
401
  };
346
402
 
347
403
  let chunk_live_status = Arc::clone(&live_status);
348
404
  let on_chunk = |summary: String| {
349
- if !options.fast_mode {
350
- if let Ok(mut status) = chunk_live_status.lock() {
351
- status.thinking = None;
352
- commit_activity_snapshot(&mut status);
353
- clear_live_status(&mut status);
354
- }
405
+ if !options.fast_mode
406
+ && let Ok(mut status) = chunk_live_status.lock()
407
+ {
408
+ status.thinking = None;
409
+ commit_activity_snapshot(&mut status);
410
+ clear_live_status(&mut status);
355
411
  }
356
412
  let formatted_summary = format_markdown_bold(&sanitize_latex(&summary));
357
413
  print!("\n {MINT}Mint:{RESET} ");
358
414
  render_live_summary(&formatted_summary);
359
415
 
360
416
  // Print web search sources if any were collected
361
- if let Ok(mut status) = chunk_live_status.lock() {
362
- if !status.web_sources.is_empty() {
363
- println!();
364
- println!(" {DIM}Sources:{RESET}");
365
- for (i, (title, url)) in status.web_sources.iter().enumerate() {
366
- println!(" {DIM}{}.{RESET} {BLUE}{}{RESET}", i + 1, title);
367
- println!(" {DIM}{}{RESET}", url);
368
- }
369
- status.web_sources.clear();
417
+ if let Ok(mut status) = chunk_live_status.lock()
418
+ && !status.web_sources.is_empty()
419
+ {
420
+ println!();
421
+ println!(" {DIM}Sources:{RESET}");
422
+ for (i, (title, url)) in status.web_sources.iter().enumerate() {
423
+ println!(" {DIM}{}.{RESET} {BLUE}{}{RESET}", i + 1, title);
424
+ println!(" {DIM}{}{RESET}", url);
370
425
  }
426
+ status.web_sources.clear();
371
427
  }
372
428
 
373
429
  println!();
@@ -378,6 +434,7 @@ pub async fn run_code_agent_with_options(
378
434
  task,
379
435
  root,
380
436
  image_data_uri,
437
+ video_data_uri,
381
438
  Some(CHAT_CLI_ID),
382
439
  None,
383
440
  options.fast_mode,
@@ -396,14 +453,14 @@ pub async fn run_code_agent_with_options(
396
453
  }
397
454
  };
398
455
  agent_done.store(true, Ordering::Relaxed);
399
- if !options.fast_mode {
400
- if let Ok(mut status) = live_status.lock() {
401
- status.thinking = None;
402
- if res.is_err() {
403
- commit_activity_snapshot(&mut status);
404
- }
405
- clear_live_status(&mut status);
456
+ if !options.fast_mode
457
+ && let Ok(mut status) = live_status.lock()
458
+ {
459
+ status.thinking = None;
460
+ if res.is_err() {
461
+ commit_activity_snapshot(&mut status);
406
462
  }
463
+ clear_live_status(&mut status);
407
464
  }
408
465
  let res = res.map_err(|e| anyhow!("{}", e))?;
409
466
 
@@ -436,19 +493,19 @@ fn parse_web_search_sources(result: &str) -> Vec<(String, String)> {
436
493
  for line in result.lines() {
437
494
  let trimmed = line.trim();
438
495
  // Match numbered title lines: "1. Title text"
439
- if let Some(rest) = trimmed.split_once(". ") {
440
- if rest.0.parse::<usize>().is_ok() {
441
- current_title = Some(rest.1.trim().to_owned());
442
- continue;
443
- }
496
+ if let Some(rest) = trimmed.split_once(". ")
497
+ && rest.0.parse::<usize>().is_ok()
498
+ {
499
+ current_title = Some(rest.1.trim().to_owned());
500
+ continue;
444
501
  }
445
502
  // Match URL lines: "URL: https://..."
446
- if let Some(url) = trimmed.strip_prefix("URL: ") {
447
- if let Some(title) = current_title.take() {
448
- let url = url.trim().to_owned();
449
- if !url.is_empty() {
450
- sources.push((title, url));
451
- }
503
+ if let Some(url) = trimmed.strip_prefix("URL: ")
504
+ && let Some(title) = current_title.take()
505
+ {
506
+ let url = url.trim().to_owned();
507
+ if !url.is_empty() {
508
+ sources.push((title, url));
452
509
  }
453
510
  }
454
511
  }
@@ -489,18 +546,73 @@ fn render_live_summary(summary: &str) {
489
546
  let _ = io::stdout().flush();
490
547
  }
491
548
 
549
+ fn diff_stats(diff: &str) -> (usize, usize) {
550
+ let mut additions = 0;
551
+ let mut deletions = 0;
552
+ for line in diff.lines() {
553
+ if line.starts_with("+++ ") || line.starts_with("--- ") {
554
+ continue;
555
+ }
556
+ if line.starts_with('+') {
557
+ additions += 1;
558
+ } else if line.starts_with('-') {
559
+ deletions += 1;
560
+ }
561
+ }
562
+ (additions, deletions)
563
+ }
564
+
565
+ fn parse_hunk_header(line: &str) -> Option<(usize, usize)> {
566
+ let trimmed = line.trim();
567
+ if !trimmed.starts_with("@@") {
568
+ return None;
569
+ }
570
+ let parts: Vec<&str> = trimmed.split("@@").collect();
571
+ if parts.len() < 3 {
572
+ return None;
573
+ }
574
+ let header_body = parts[1].trim();
575
+ let mut old_start = 1;
576
+ let mut new_start = 1;
577
+
578
+ for token in header_body.split_whitespace() {
579
+ if let Some(num_str) = token.strip_prefix('-') {
580
+ let start_str = num_str.split(',').next().unwrap_or(num_str);
581
+ old_start = start_str.parse().unwrap_or(1);
582
+ } else if let Some(num_str) = token.strip_prefix('+') {
583
+ let start_str = num_str.split(',').next().unwrap_or(num_str);
584
+ new_start = start_str.parse().unwrap_or(1);
585
+ }
586
+ }
587
+ Some((old_start, new_start))
588
+ }
589
+
492
590
  fn print_colored_diff(diff: &str) {
591
+ let mut current_old_line = 1;
592
+ let mut current_new_line = 1;
593
+
493
594
  for line in diff.lines() {
494
595
  if line.starts_with("@@") {
495
- println!("{BLUE}{line}{RESET}");
596
+ if let Some((old_s, new_s)) = parse_hunk_header(line) {
597
+ current_old_line = old_s;
598
+ current_new_line = new_s;
599
+ }
600
+ println!(" {BLUE}{line}{RESET}");
496
601
  } else if line.starts_with("--- ") || line.starts_with("+++ ") {
497
- println!("{DIM}{line}{RESET}");
602
+ println!(" {DIM}{line}{RESET}");
498
603
  } else if line.starts_with('-') {
499
- println!("\x1b[31m{line}\x1b[0m");
604
+ let line_num_str = format!("{:>5}", current_old_line);
605
+ current_old_line += 1;
606
+ println!(" {DIM}{line_num_str}{RESET} \x1b[31m{line}\x1b[0m");
500
607
  } else if line.starts_with('+') {
501
- println!("\x1b[32m{line}\x1b[0m");
608
+ let line_num_str = format!("{:>5}", current_new_line);
609
+ current_new_line += 1;
610
+ println!(" {DIM}{line_num_str}{RESET} \x1b[32m{line}\x1b[0m");
502
611
  } else {
503
- println!("{line}");
612
+ let line_num_str = format!("{:>5}", current_new_line);
613
+ current_old_line += 1;
614
+ current_new_line += 1;
615
+ println!(" {DIM}{line_num_str}{RESET} {line}");
504
616
  }
505
617
  }
506
618
  }
@@ -640,6 +752,80 @@ fn is_thai_combining(c: char) -> bool {
640
752
  )
641
753
  }
642
754
 
755
+ fn apply_wave_effect(text: &str, tick: usize) -> String {
756
+ let (label, metadata) = if let Some(idx) = text.rfind(" โ€ข Esc to interrupt)") {
757
+ if let Some(open_paren_idx) = text[..idx].rfind('(') {
758
+ (&text[..open_paren_idx], &text[open_paren_idx..])
759
+ } else {
760
+ (text, "")
761
+ }
762
+ } else {
763
+ (text, "")
764
+ };
765
+
766
+ let trimmed_label = label.trim_end();
767
+ let spaces_count = label.len() - trimmed_label.len();
768
+
769
+ let chars: Vec<char> = trimmed_label.chars().collect();
770
+ let n = chars.len();
771
+ if n == 0 {
772
+ return text.to_string();
773
+ }
774
+
775
+ let mut animated_label = String::new();
776
+
777
+ // Smooth wave movement using a sine wave function.
778
+ // phase shifts with tick (time), index i acts as spatial shift.
779
+ // 0.3 speed controls animation rate, 0.4 controls width of the wave crest.
780
+ let speed = 0.3;
781
+ let phase = tick as f32 * speed;
782
+
783
+ for (i, &c) in chars.iter().enumerate() {
784
+ if c == ' ' {
785
+ animated_label.push(c);
786
+ continue;
787
+ }
788
+
789
+ let x = (i as f32 * 0.4) - phase;
790
+ let t = (x.sin() + 1.0) / 2.0; // Oscillates in [0.0, 1.0]
791
+
792
+ // Stop colors: Dim Gray (70, 70, 70) -> Mint Green (105, 230, 166) -> Cyan (78, 201, 216)
793
+ let (r, g, b) = if t < 0.3 {
794
+ let local_t = t / 0.3;
795
+ let r = 70.0 + (105.0 - 70.0) * local_t;
796
+ let g = 70.0 + (230.0 - 70.0) * local_t;
797
+ let b = 70.0 + (166.0 - 70.0) * local_t;
798
+ (r, g, b)
799
+ } else if t < 0.7 {
800
+ let local_t = (t - 0.3) / 0.4;
801
+ let r = 105.0 + (78.0 - 105.0) * local_t;
802
+ let g = 230.0 + (201.0 - 230.0) * local_t;
803
+ let b = 166.0 + (216.0 - 166.0) * local_t;
804
+ (r, g, b)
805
+ } else {
806
+ let local_t = (t - 0.7) / 0.3;
807
+ let r = 78.0 + (70.0 - 78.0) * local_t;
808
+ let g = 201.0 + (70.0 - 201.0) * local_t;
809
+ let b = 216.0 + (70.0 - 216.0) * local_t;
810
+ (r, g, b)
811
+ };
812
+
813
+ animated_label.push_str(&format!(
814
+ "\x1b[1m\x1b[38;2;{};{};{}m{}\x1b[0m",
815
+ r.round() as u8,
816
+ g.round() as u8,
817
+ b.round() as u8,
818
+ c
819
+ ));
820
+ }
821
+
822
+ animated_label.push_str(&" ".repeat(spaces_count));
823
+ if !metadata.is_empty() {
824
+ animated_label.push_str(&format!("{BRIGHT}{metadata}{RESET}"));
825
+ }
826
+ animated_label
827
+ }
828
+
643
829
  fn render_live_status(status: &mut LiveStatus) {
644
830
  clear_live_status(status);
645
831
  let mut lines = Vec::new();
@@ -663,10 +849,32 @@ fn render_live_status(status: &mut LiveStatus) {
663
849
  tick,
664
850
  ));
665
851
  if let Some(thinking) = &status.thinking {
666
- let frames = &["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "];
852
+ let frames = &[
853
+ "๐ŸŒ‘\u{FE0E}",
854
+ "๐ŸŒ’\u{FE0E}",
855
+ "๐ŸŒ“\u{FE0E}",
856
+ "๐ŸŒ”\u{FE0E}",
857
+ "๐ŸŒ•\u{FE0E}",
858
+ "๐ŸŒ–\u{FE0E}",
859
+ "๐ŸŒ—\u{FE0E}",
860
+ "๐ŸŒ˜\u{FE0E}",
861
+ ];
667
862
  let frame = frames[status.spinner_tick % frames.len()];
863
+
864
+ let dots_frames = &["", ".", "..", "..."];
865
+ let dots = dots_frames[(status.spinner_tick / 2) % dots_frames.len()];
866
+
668
867
  status.spinner_tick += 1;
669
- lines.push(format!(" {MINT}{frame}{RESET} {BRIGHT}{thinking}{RESET}"));
868
+
869
+ let custom_thinking = if thinking.contains("is thinking") {
870
+ thinking.replace("is thinking", &format!("is thinking{:<3}", dots))
871
+ } else {
872
+ thinking.replace("Thinking", &format!("Thinking{:<3}", dots))
873
+ };
874
+
875
+ let waved_thinking = apply_wave_effect(&custom_thinking, status.spinner_tick);
876
+
877
+ lines.push(format!(" {MINT}{frame}{RESET} {waved_thinking}"));
670
878
  }
671
879
  if lines.is_empty() {
672
880
  return;
@@ -680,7 +888,7 @@ fn render_live_status(status: &mut LiveStatus) {
680
888
  let stripped = strip_ansi_escapes(line);
681
889
  let line_len = stripped.chars().filter(|&c| !is_thai_combining(c)).count();
682
890
  let physical_lines = if width > 0 {
683
- (line_len + width - 1) / width
891
+ line_len.div_ceil(width)
684
892
  } else {
685
893
  1
686
894
  }
@@ -753,7 +961,11 @@ fn clear_live_status(status: &mut LiveStatus) {
753
961
 
754
962
  fn get_bullet(name: &str, is_thinking: bool, tick: usize) -> String {
755
963
  let char_str = if is_thinking {
756
- if (tick / 4) % 2 == 0 { "โ—" } else { "โ—‹" }
964
+ if (tick / 4).is_multiple_of(2) {
965
+ "โ—"
966
+ } else {
967
+ "โ—‹"
968
+ }
757
969
  } else {
758
970
  "โ—"
759
971
  };
@@ -936,6 +1148,33 @@ async fn wait_for_escape_interrupt(approval_active: Arc<AtomicBool>) {
936
1148
  }
937
1149
  }
938
1150
 
1151
+ fn print_approval_card(title: &str, fields: &[(&str, &str)]) {
1152
+ let top_bar = format!(" {DIM}{}โ”ฌ{}{RESET}", "โ”€".repeat(11), "โ”€".repeat(56));
1153
+ let bot_bar = format!(" {DIM}{}โ”ด{}{RESET}", "โ”€".repeat(11), "โ”€".repeat(56));
1154
+
1155
+ println!();
1156
+ println!(
1157
+ " {BRIGHT}APPROVAL REQUIRED{RESET} {DIM}โ€ข{RESET} {BLUE}{}{RESET}",
1158
+ title
1159
+ );
1160
+ println!("{}", top_bar);
1161
+ for (label, val) in fields {
1162
+ let val_lines: Vec<&str> = val.lines().collect();
1163
+ if val_lines.is_empty() {
1164
+ println!(" {BRIGHT}{:<10}{RESET} {DIM}โ”‚{RESET}", label);
1165
+ } else {
1166
+ for (idx, line) in val_lines.iter().enumerate() {
1167
+ if idx == 0 {
1168
+ println!(" {BRIGHT}{:<10}{RESET} {DIM}โ”‚{RESET} {}", label, line);
1169
+ } else {
1170
+ println!(" {DIM}โ”‚{RESET} {}", line);
1171
+ }
1172
+ }
1173
+ }
1174
+ }
1175
+ println!("{}", bot_bar);
1176
+ }
1177
+
939
1178
  fn confirm_pausing_interrupt(prompt: &str, approval_active: &AtomicBool) -> bool {
940
1179
  approval_active.store(true, Ordering::Relaxed);
941
1180
  let approved = crate::confirm(prompt).unwrap_or(false);
@@ -966,7 +1205,7 @@ fn format_markdown_bold(text: &str) -> String {
966
1205
  } else {
967
1206
  if c == '-' || c == '*' || c == '+' {
968
1207
  let after = &line[idx + c.len_utf8()..];
969
- if after.chars().next().map_or(false, |c| c.is_whitespace()) {
1208
+ if after.chars().next().is_some_and(|c| c.is_whitespace()) {
970
1209
  is_list_item = true;
971
1210
  marker_char = c;
972
1211
  }
@@ -1043,27 +1282,43 @@ fn sanitize_latex(text: &str) -> String {
1043
1282
  let mut s = text.to_owned();
1044
1283
  for (pat, uni) in [
1045
1284
  // arrows
1046
- ("$\\rightarrow$", "โ†’"),
1047
- ("$\\leftarrow$", "โ†"),
1048
- ("$\\Rightarrow$", "โ‡’"),
1049
- ("$\\Leftarrow$", "โ‡"),
1285
+ ("$\\rightarrow$", "โ†’"),
1286
+ ("\\rightarrow", "โ†’"),
1287
+ ("ightarrow", "โ†’"),
1288
+ ("$\\leftarrow$", "โ†"),
1289
+ ("\\leftarrow", "โ†"),
1290
+ ("eftarrow", "โ†"),
1291
+ ("$\\Rightarrow$", "โ‡’"),
1292
+ ("\\Rightarrow", "โ‡’"),
1293
+ ("$\\Leftarrow$", "โ‡"),
1294
+ ("\\Leftarrow$", "โ‡"),
1050
1295
  ("$\\leftrightarrow$", "โ†”"),
1296
+ ("\\leftrightarrow", "โ†”"),
1051
1297
  // comparison
1052
- ("$\\leq$", "โ‰ค"),
1053
- ("$\\geq$", "โ‰ฅ"),
1054
- ("$\\neq$", "โ‰ "),
1298
+ ("$\\leq$", "โ‰ค"),
1299
+ ("\\leq", "โ‰ค"),
1300
+ ("$\\geq$", "โ‰ฅ"),
1301
+ ("\\geq", "โ‰ฅ"),
1302
+ ("$\\neq$", "โ‰ "),
1303
+ ("\\neq", "โ‰ "),
1055
1304
  ("$\\approx$", "โ‰ˆ"),
1305
+ ("\\approx", "โ‰ˆ"),
1056
1306
  // math
1057
1307
  ("$\\times$", "ร—"),
1058
- ("$\\div$", "รท"),
1059
- ("$\\pm$", "ยฑ"),
1308
+ ("\\times", "ร—"),
1309
+ ("$\\div$", "รท"),
1310
+ ("\\div", "รท"),
1311
+ ("$\\pm$", "ยฑ"),
1312
+ ("\\pm", "ยฑ"),
1060
1313
  ("$\\infty$", "โˆž"),
1061
- ("$\\cdot$", "ยท"),
1314
+ ("\\infty", "โˆž"),
1315
+ ("$\\cdot$", "ยท"),
1316
+ ("\\cdot", "ยท"),
1062
1317
  // sets
1063
- ("$\\in$", "โˆˆ"),
1318
+ ("$\\in$", "โˆˆ"),
1064
1319
  ("$\\subset$", "โŠ‚"),
1065
- ("$\\cup$", "โˆช"),
1066
- ("$\\cap$", "โˆฉ"),
1320
+ ("$\\cup$", "โˆช"),
1321
+ ("$\\cap$", "โˆฉ"),
1067
1322
  ] {
1068
1323
  s = s.replace(pat, uni);
1069
1324
  }