@pheem49/mint 1.9.0 → 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 (90) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +285 -140
  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 +287 -43
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/interactive.rs +2888 -0
  10. package/crates/mint-cli/src/main.rs +336 -2854
  11. package/crates/mint-cli/src/markdown.rs +294 -0
  12. package/crates/mint-cli/src/onboard.rs +44 -1
  13. package/crates/mint-cli/src/updater.rs +48 -6
  14. package/crates/mint-core/Cargo.toml +1 -0
  15. package/crates/mint-core/src/agent_loop.rs +1 -0
  16. package/crates/mint-core/src/api_server.rs +376 -13
  17. package/crates/mint-core/src/browser.rs +629 -101
  18. package/crates/mint-core/src/channels.rs +105 -6
  19. package/crates/mint-core/src/chat.rs +52 -4
  20. package/crates/mint-core/src/code_tools.rs +29 -11
  21. package/crates/mint-core/src/config.rs +118 -0
  22. package/crates/mint-core/src/image_gen.rs +294 -33
  23. package/crates/mint-core/src/lib.rs +5 -2
  24. package/crates/mint-core/src/mcp.rs +119 -6
  25. package/crates/mint-core/src/orchestration.rs +133 -11
  26. package/crates/mint-core/src/pictures.rs +64 -1
  27. package/crates/mint-core/src/skills.rs +82 -6
  28. package/crates/mint-core/src/video_gen.rs +235 -0
  29. package/package.json +8 -2
  30. package/shared-modules/luajit/luajit.json +2 -2
  31. package/skills-lock.json +41 -0
  32. package/src/renderer/index-web.html +1 -1
  33. package/src/renderer/index.html +1 -1
  34. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  35. package/src/renderer/shared/platform.ts +3 -0
  36. package/src/renderer/shared/types.ts +4 -0
  37. package/src/renderer/shared/utils/approval.tsx +86 -7
  38. package/src/renderer/shared/utils/markdown.tsx +217 -88
  39. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  40. package/src/renderer/src/components/ChatPanel.tsx +461 -64
  41. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  42. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  43. package/src/renderer/src/components/MintDashboard.tsx +150 -5
  44. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  45. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  46. package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
  47. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  48. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  49. package/src/renderer/src/components/SettingsWindow.tsx +38 -2
  50. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  51. package/src/renderer/src/css/image-studio.css +106 -0
  52. package/src/renderer/src/css/settings.css +158 -0
  53. package/src/renderer/src/css/styles.css +352 -1
  54. package/src/renderer/src/css/veo-studio.css +626 -0
  55. package/src/renderer/src/index.css +1 -0
  56. package/src/renderer/src/tauri.ts +144 -17
  57. package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
  58. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  59. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  60. package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
  61. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  62. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  63. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
  64. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  65. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  66. package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
  67. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  68. package/src/renderer/src-web/css/image-studio.css +106 -0
  69. package/src/renderer/src-web/css/settings.css +158 -0
  70. package/src/renderer/src-web/css/styles.css +353 -1
  71. package/src/renderer/src-web/css/veo-studio.css +650 -0
  72. package/src/renderer/src-web/index.css +1 -0
  73. package/src/renderer/src-web/tauri.ts +135 -16
  74. package/src-tauri/src/lib.rs +114 -8
  75. package/src-tauri/src/webhooks.rs +45 -31
  76. package/src-tauri/src/workflows.rs +1 -0
  77. package/src-tauri/tauri.conf.json +1 -1
  78. package/temp_gridgeist/AGENTS.md +8 -0
  79. package/temp_gridgeist/CHANGELOG.md +130 -0
  80. package/temp_gridgeist/LICENSE +21 -0
  81. package/temp_gridgeist/README.md +323 -0
  82. package/temp_gridgeist/README.th.md +325 -0
  83. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  84. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  85. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  86. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  87. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  88. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  89. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  90. 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) {
@@ -385,6 +434,7 @@ pub async fn run_code_agent_with_options(
385
434
  task,
386
435
  root,
387
436
  image_data_uri,
437
+ video_data_uri,
388
438
  Some(CHAT_CLI_ID),
389
439
  None,
390
440
  options.fast_mode,
@@ -496,18 +546,73 @@ fn render_live_summary(summary: &str) {
496
546
  let _ = io::stdout().flush();
497
547
  }
498
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
+
499
590
  fn print_colored_diff(diff: &str) {
591
+ let mut current_old_line = 1;
592
+ let mut current_new_line = 1;
593
+
500
594
  for line in diff.lines() {
501
595
  if line.starts_with("@@") {
502
- 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}");
503
601
  } else if line.starts_with("--- ") || line.starts_with("+++ ") {
504
- println!("{DIM}{line}{RESET}");
602
+ println!(" {DIM}{line}{RESET}");
505
603
  } else if line.starts_with('-') {
506
- 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");
507
607
  } else if line.starts_with('+') {
508
- 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");
509
611
  } else {
510
- 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}");
511
616
  }
512
617
  }
513
618
  }
@@ -647,6 +752,80 @@ fn is_thai_combining(c: char) -> bool {
647
752
  )
648
753
  }
649
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
+
650
829
  fn render_live_status(status: &mut LiveStatus) {
651
830
  clear_live_status(status);
652
831
  let mut lines = Vec::new();
@@ -670,10 +849,32 @@ fn render_live_status(status: &mut LiveStatus) {
670
849
  tick,
671
850
  ));
672
851
  if let Some(thinking) = &status.thinking {
673
- 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
+ ];
674
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
+
675
867
  status.spinner_tick += 1;
676
- 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}"));
677
878
  }
678
879
  if lines.is_empty() {
679
880
  return;
@@ -947,6 +1148,33 @@ async fn wait_for_escape_interrupt(approval_active: Arc<AtomicBool>) {
947
1148
  }
948
1149
  }
949
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
+
950
1178
  fn confirm_pausing_interrupt(prompt: &str, approval_active: &AtomicBool) -> bool {
951
1179
  approval_active.store(true, Ordering::Relaxed);
952
1180
  let approved = crate::confirm(prompt).unwrap_or(false);
@@ -1055,21 +1283,37 @@ fn sanitize_latex(text: &str) -> String {
1055
1283
  for (pat, uni) in [
1056
1284
  // arrows
1057
1285
  ("$\\rightarrow$", "→"),
1286
+ ("\\rightarrow", "→"),
1287
+ ("ightarrow", "→"),
1058
1288
  ("$\\leftarrow$", "←"),
1289
+ ("\\leftarrow", "←"),
1290
+ ("eftarrow", "←"),
1059
1291
  ("$\\Rightarrow$", "⇒"),
1292
+ ("\\Rightarrow", "⇒"),
1060
1293
  ("$\\Leftarrow$", "⇐"),
1294
+ ("\\Leftarrow$", "⇐"),
1061
1295
  ("$\\leftrightarrow$", "↔"),
1296
+ ("\\leftrightarrow", "↔"),
1062
1297
  // comparison
1063
1298
  ("$\\leq$", "≤"),
1299
+ ("\\leq", "≤"),
1064
1300
  ("$\\geq$", "≥"),
1301
+ ("\\geq", "≥"),
1065
1302
  ("$\\neq$", "≠"),
1303
+ ("\\neq", "≠"),
1066
1304
  ("$\\approx$", "≈"),
1305
+ ("\\approx", "≈"),
1067
1306
  // math
1068
1307
  ("$\\times$", "×"),
1308
+ ("\\times", "×"),
1069
1309
  ("$\\div$", "÷"),
1310
+ ("\\div", "÷"),
1070
1311
  ("$\\pm$", "±"),
1312
+ ("\\pm", "±"),
1071
1313
  ("$\\infty$", "∞"),
1314
+ ("\\infty", "∞"),
1072
1315
  ("$\\cdot$", "·"),
1316
+ ("\\cdot", "·"),
1073
1317
  // sets
1074
1318
  ("$\\in$", "∈"),
1075
1319
  ("$\\subset$", "⊂"),
@@ -37,8 +37,28 @@ pub async fn auth(no_open: bool, port: u16) -> Result<()> {
37
37
  if !no_open {
38
38
  open_browser(&url)?;
39
39
  }
40
- println!("Waiting for Gmail OAuth callback on {redirect} ...");
41
- let code = wait_for_code(listener, &state)?;
40
+ use indicatif::{ProgressBar, ProgressStyle};
41
+ use std::time::Duration;
42
+ let spinner = ProgressBar::new_spinner();
43
+ spinner.set_style(
44
+ ProgressStyle::default_spinner()
45
+ .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
46
+ .template("{spinner:.green} {msg}")
47
+ .unwrap(),
48
+ );
49
+ spinner.set_message("Waiting for Gmail OAuth authorization in browser...");
50
+ spinner.enable_steady_tick(Duration::from_millis(80));
51
+
52
+ let code = match wait_for_code(listener, &state) {
53
+ Ok(c) => {
54
+ spinner.finish_and_clear();
55
+ c
56
+ }
57
+ Err(e) => {
58
+ spinner.finish_and_clear();
59
+ return Err(e);
60
+ }
61
+ };
42
62
  let token: Value = mint_core::HTTP_CLIENT
43
63
  .clone()
44
64
  .post(TOKEN_URL)