@pheem49/mint 1.7.1 → 1.8.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 (66) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +40 -0
  5. package/Release_Note.md +95 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/crates/mint-cli/Cargo.toml +1 -0
  8. package/crates/mint-cli/src/agent.rs +142 -48
  9. package/crates/mint-cli/src/main.rs +231 -22
  10. package/crates/mint-core/src/api_server.rs +71 -4
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/chat.rs +79 -0
  13. package/crates/mint-core/src/config.rs +69 -1
  14. package/crates/mint-core/src/lib.rs +6 -1
  15. package/crates/mint-core/src/orchestration.rs +137 -1
  16. package/crates/mint-core/src/skills.rs +113 -4
  17. package/crates/mint-core/src/workflows.rs +9 -22
  18. package/package.json +2 -1
  19. package/src/renderer/src/App.tsx +1 -1
  20. package/src/renderer/src/components/ChatPanel.tsx +197 -47
  21. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  22. package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
  23. package/src/renderer/src/components/MintDashboard.tsx +239 -7
  24. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  25. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  26. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  27. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  28. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  29. package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
  30. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  31. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  32. package/src/renderer/src/components/SettingsWindow.tsx +175 -970
  33. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  34. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  35. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  36. package/src/renderer/src/css/image-studio.css +15 -4
  37. package/src/renderer/src/css/settings.css +75 -6
  38. package/src/renderer/src/css/styles.css +419 -49
  39. package/src/renderer/src/css/workflows.css +542 -0
  40. package/src/renderer/src/env.d.ts +2 -0
  41. package/src/renderer/src/index.css +13 -1
  42. package/src/renderer/src/tauri.ts +140 -2
  43. package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
  44. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  45. package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
  46. package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
  47. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  48. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  49. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  50. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  51. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  52. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
  53. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  54. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  55. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
  56. package/src/renderer/src-web/css/image-studio.css +15 -4
  57. package/src/renderer/src-web/css/settings.css +75 -6
  58. package/src/renderer/src-web/css/styles.css +394 -50
  59. package/src/renderer/src-web/env.d.ts +2 -0
  60. package/src/renderer/src-web/index.css +12 -1
  61. package/src/renderer/src-web/tauri.ts +60 -2
  62. package/src-tauri/src/browser.rs +1 -141
  63. package/src-tauri/src/headless.rs +3 -2
  64. package/src-tauri/src/lib.rs +195 -111
  65. package/src-tauri/src/workflows.rs +56 -4
  66. package/src-tauri/tauri.conf.json +1 -1
@@ -1,4 +1,3 @@
1
- mod browser;
2
1
  mod desktop;
3
2
  mod discord_rpc;
4
3
  mod events;
@@ -11,8 +10,7 @@ mod updater;
11
10
  mod webhooks;
12
11
  mod workflows;
13
12
 
14
- use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
15
- use browser::{
13
+ use mint_core::browser::{
16
14
  BrowserTab, click as browser_click, list_tabs as browser_list_tabs,
17
15
  navigate as browser_navigate, read_page_text,
18
16
  };
@@ -34,15 +32,15 @@ use mint_core::{
34
32
  AgentApproval, AgentProgress, AppliedCodeEdit, ApprovalOutcome, ChatRequest, ChatResponse,
35
33
  ChatSession, CodeEdit, CodeEditProposal, ImageGenRequest, InteractionMemory, MemoryStore,
36
34
  MintConfig, PictureEntry, TtsUrl, WeatherReport, apply_code_edits, classify_shell_command,
37
- config_path, extract_document_text, google_tts_urls, list_saved_pictures, load_config,
38
- load_workflows, orchestrate_agent_loop, orchestrate_chat_stream_with_fallback,
39
- orchestrate_chat_with_fallback, propose_code_edits, save_chat_images, save_config,
40
- start_channels, weather, workflows_path,
35
+ config_path, google_tts_urls, list_saved_pictures, load_config, load_workflows,
36
+ orchestrate_agent_loop, orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
37
+ propose_code_edits, save_chat_images, save_config, save_workflows, start_channels, weather,
38
+ workflows_path,
41
39
  };
42
40
  use plugins::execute_plugin;
43
41
 
44
42
  pub struct ApprovalsState {
45
- pub pending: Mutex<HashMap<String, oneshot::Sender<bool>>>,
43
+ pub pending: Mutex<HashMap<String, oneshot::Sender<ApprovalOutcome>>>,
46
44
  }
47
45
 
48
46
  static COUNTER: AtomicU64 = AtomicU64::new(1);
@@ -94,9 +92,6 @@ enum DesktopStreamEvent {
94
92
  Chunk { chunk: String },
95
93
  Progress { progress: AgentProgress },
96
94
  }
97
-
98
- const MAX_DOCUMENT_BYTES: usize = 10 * 1024 * 1024;
99
- const MAX_DOCUMENT_CONTEXT_CHARS: usize = 30_000;
100
95
  const WORKSPACE_TREE_MAX_DEPTH: usize = 9;
101
96
  const WORKSPACE_TREE_MAX_CHILDREN: usize = 400;
102
97
  const WORKSPACE_TREE_COLLAPSED_DIRS: &[&str] = &[
@@ -114,85 +109,6 @@ const WORKSPACE_TREE_COLLAPSED_DIRS: &[&str] = &[
114
109
  "target",
115
110
  ];
116
111
 
117
- fn request_with_document_context(
118
- config: &MintConfig,
119
- request: &ChatRequest,
120
- ) -> Result<ChatRequest, String> {
121
- let Some(document) = &request.document_attachment else {
122
- return Ok(request.clone());
123
- };
124
-
125
- let (mime_type, encoded) = document
126
- .data_uri
127
- .strip_prefix("data:")
128
- .and_then(|payload| payload.split_once(";base64,"))
129
- .ok_or_else(|| "invalid PDF attachment data URI".to_string())?;
130
-
131
- if !mime_type.eq_ignore_ascii_case("application/pdf") {
132
- return Err("only PDF document attachments are supported".into());
133
- }
134
-
135
- let bytes = BASE64_STANDARD
136
- .decode(encoded)
137
- .map_err(|error| format!("invalid PDF attachment encoding: {error}"))?;
138
- if bytes.len() > MAX_DOCUMENT_BYTES {
139
- return Err("PDF attachment is too large (> 10 MiB)".into());
140
- }
141
-
142
- let directory = config_path()
143
- .map_err(|error| error.to_string())?
144
- .with_file_name("Attachments");
145
- fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
146
- let path = directory.join(format!(
147
- "mint-pdf-{}-{}.pdf",
148
- COUNTER.fetch_add(1, Ordering::SeqCst),
149
- sanitize_attachment_name(&document.filename)
150
- ));
151
- fs::write(&path, bytes).map_err(|error| error.to_string())?;
152
-
153
- let extracted = extract_document_text(&path, config).map_err(|error| error.to_string());
154
- let _ = fs::remove_file(&path);
155
- let extracted = extracted?;
156
- let context = truncate_document_context(&extracted);
157
- let filename = document.filename.trim();
158
- let filename = if filename.is_empty() {
159
- "attached.pdf"
160
- } else {
161
- filename
162
- };
163
-
164
- let mut enriched = request.clone();
165
- enriched.document_attachment = None;
166
- enriched.message = format!(
167
- "{}\n\nAttached PDF: {filename}\nUse the extracted PDF text below when answering. If the user asks for a summary, summarize this document.\n\n--- Extracted PDF text ---\n{context}\n--- End extracted PDF text ---",
168
- request.message
169
- );
170
- Ok(enriched)
171
- }
172
-
173
- fn truncate_document_context(text: &str) -> String {
174
- let mut output: String = text.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
175
- if text.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
176
- output.push_str("\n\n[PDF text truncated because it is long.]");
177
- }
178
- output
179
- }
180
-
181
- fn sanitize_attachment_name(filename: &str) -> String {
182
- let sanitized: String = filename
183
- .chars()
184
- .filter(|character| {
185
- character.is_ascii_alphanumeric() || matches!(character, '.' | '-' | '_')
186
- })
187
- .take(80)
188
- .collect();
189
- if sanitized.is_empty() {
190
- "document".into()
191
- } else {
192
- sanitized
193
- }
194
- }
195
-
196
112
  #[tauri::command]
197
113
  fn get_runtime_status() -> Result<RuntimeStatus, String> {
198
114
  let config = load_config().map_err(|error| error.to_string())?;
@@ -219,6 +135,26 @@ async fn get_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry,
219
135
  .map_err(|error| format!("workspace tree task failed: {error}"))?
220
136
  }
221
137
 
138
+ #[tauri::command]
139
+ async fn create_workspace_file(path: String) -> Result<(), String> {
140
+ std::fs::write(&path, "").map_err(|error| error.to_string())
141
+ }
142
+
143
+ #[tauri::command]
144
+ async fn create_workspace_folder(path: String) -> Result<(), String> {
145
+ std::fs::create_dir_all(&path).map_err(|error| error.to_string())
146
+ }
147
+
148
+ #[tauri::command]
149
+ async fn delete_workspace_item(path: String) -> Result<(), String> {
150
+ let path_buf = std::path::PathBuf::from(path);
151
+ if path_buf.is_dir() {
152
+ std::fs::remove_dir_all(path_buf).map_err(|error| error.to_string())
153
+ } else {
154
+ std::fs::remove_file(path_buf).map_err(|error| error.to_string())
155
+ }
156
+ }
157
+
222
158
  fn build_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry, String> {
223
159
  let root = workspace_root(path.as_deref())?;
224
160
  let name = root
@@ -274,6 +210,11 @@ fn workspace_root(path: Option<&str>) -> Result<PathBuf, String> {
274
210
  None => std::env::current_dir().map_err(|error| error.to_string())?,
275
211
  };
276
212
  let root = root.canonicalize().map_err(|error| error.to_string())?;
213
+ let root = if root.ends_with("src-tauri") {
214
+ root.parent().unwrap_or(&root).to_path_buf()
215
+ } else {
216
+ root
217
+ };
277
218
  if !root.is_dir() {
278
219
  return Err(format!("workspace is not a directory: {}", root.display()));
279
220
  }
@@ -376,7 +317,7 @@ fn inspect_shell_command(command: String) -> mint_core::ShellClassification {
376
317
  #[tauri::command]
377
318
  async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatResponse, String> {
378
319
  let config = load_config().map_err(|error| error.to_string())?;
379
- let request = request_with_document_context(&config, &request)?;
320
+ let request = request.with_document_context(&config)?;
380
321
 
381
322
  if request.message.starts_with("/chat ") {
382
323
  let mut clean_request = request.clone();
@@ -413,15 +354,10 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
413
354
  )
414
355
  .map_err(|e| e.to_string())?;
415
356
 
416
- let approved =
357
+ let outcome =
417
358
  tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
418
- .unwrap_or(false);
419
-
420
- if approved {
421
- Ok(ApprovalOutcome::Approved)
422
- } else {
423
- Ok(ApprovalOutcome::Denied)
424
- }
359
+ .unwrap_or(ApprovalOutcome::Denied);
360
+ Ok(outcome)
425
361
  };
426
362
 
427
363
  let progress_cb = |_| {};
@@ -456,7 +392,7 @@ async fn stream_chat_message(
456
392
  on_event: Channel<DesktopStreamEvent>,
457
393
  ) -> Result<ChatResponse, String> {
458
394
  let config = load_config().map_err(|error| error.to_string())?;
459
- let request = request_with_document_context(&config, &request)?;
395
+ let request = request.with_document_context(&config)?;
460
396
 
461
397
  if request.message.starts_with("/chat ") {
462
398
  let mut clean_request = request.clone();
@@ -496,15 +432,10 @@ async fn stream_chat_message(
496
432
  )
497
433
  .map_err(|e| e.to_string())?;
498
434
 
499
- let approved =
435
+ let outcome =
500
436
  tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
501
- .unwrap_or(false);
502
-
503
- if approved {
504
- Ok(ApprovalOutcome::Approved)
505
- } else {
506
- Ok(ApprovalOutcome::Denied)
507
- }
437
+ .unwrap_or(ApprovalOutcome::Denied);
438
+ Ok(outcome)
508
439
  };
509
440
 
510
441
  let on_progress_event = on_event.clone();
@@ -562,6 +493,20 @@ fn save_interaction_agent_activity(
562
493
  .map_err(|error| error.to_string())
563
494
  }
564
495
 
496
+ #[tauri::command]
497
+ fn save_system_interaction(
498
+ chat_id: String,
499
+ user_text: String,
500
+ provider: String,
501
+ model: String,
502
+ ) -> Result<i64, String> {
503
+ MemoryStore::open_default()
504
+ .and_then(|memory| {
505
+ memory.add_interaction_for_chat(&chat_id, &user_text, "", &provider, &model)
506
+ })
507
+ .map_err(|error| error.to_string())
508
+ }
509
+
565
510
  #[tauri::command]
566
511
  fn get_recent_interactions(
567
512
  limit: Option<usize>,
@@ -614,6 +559,87 @@ fn set_profile_value(key: String, value: String) -> Result<(), String> {
614
559
  .map_err(|error| error.to_string())
615
560
  }
616
561
 
562
+ #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
563
+ #[serde(rename_all = "camelCase")]
564
+ struct LearnedSkillDto {
565
+ id: i64,
566
+ name: String,
567
+ source_path: String,
568
+ content: String,
569
+ updated_at: String,
570
+ location: String,
571
+ }
572
+
573
+ #[tauri::command]
574
+ fn list_learned_skills(workspace_path: Option<String>) -> Result<Vec<LearnedSkillDto>, String> {
575
+ let store = MemoryStore::open_default().map_err(|e| e.to_string())?;
576
+ let db_skills = store.learned_skills(100).map_err(|e| e.to_string())?;
577
+
578
+ let mut global_skills = Vec::new();
579
+ if let Some(home) = dirs::home_dir() {
580
+ let global_skills_path = home.join(".config").join("mint").join("mint-skills");
581
+ if !global_skills_path.exists() {
582
+ let _ = std::fs::create_dir_all(&global_skills_path);
583
+ }
584
+ mint_core::skills::load_skills_from_dir(&global_skills_path, &mut global_skills);
585
+ }
586
+
587
+ let mut workspace_skills = Vec::new();
588
+ if let Ok(root) = workspace_root(workspace_path.as_deref()) {
589
+ let workspace_skills_path1 = root.join(".agents").join("skills");
590
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut workspace_skills);
591
+
592
+ let workspace_skills_path2 = root.join("skills");
593
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut workspace_skills);
594
+ }
595
+
596
+ let mut unique_skills = std::collections::BTreeMap::new();
597
+ for s in db_skills {
598
+ unique_skills.insert(s.name.clone(), (s, "database"));
599
+ }
600
+ for s in global_skills {
601
+ unique_skills.insert(s.name.clone(), (s, "global"));
602
+ }
603
+ for s in workspace_skills {
604
+ unique_skills.insert(s.name.clone(), (s, "workspace"));
605
+ }
606
+
607
+ let dtos = unique_skills
608
+ .into_values()
609
+ .map(|(s, loc)| LearnedSkillDto {
610
+ id: s.id,
611
+ name: s.name,
612
+ source_path: s.source_path,
613
+ content: s.content,
614
+ updated_at: s.created_at,
615
+ location: loc.to_string(),
616
+ })
617
+ .collect();
618
+ Ok(dtos)
619
+ }
620
+
621
+ #[tauri::command]
622
+ fn add_learned_skill(name: String, content: String) -> Result<LearnedSkillDto, String> {
623
+ let store = MemoryStore::open_default().map_err(|e| e.to_string())?;
624
+ let skill = store
625
+ .add_learned_skill(&name, "ui_manual", &content)
626
+ .map_err(|e| e.to_string())?;
627
+ Ok(LearnedSkillDto {
628
+ id: skill.id,
629
+ name: skill.name,
630
+ source_path: skill.source_path,
631
+ content: skill.content,
632
+ updated_at: skill.created_at,
633
+ location: "database".to_string(),
634
+ })
635
+ }
636
+
637
+ #[tauri::command]
638
+ fn delete_learned_skill(name: String) -> Result<usize, String> {
639
+ let store = MemoryStore::open_default().map_err(|e| e.to_string())?;
640
+ store.delete_learned_skill(&name).map_err(|e| e.to_string())
641
+ }
642
+
617
643
  #[tauri::command]
618
644
  fn clear_chat_history(chat_id: Option<String>) -> Result<usize, String> {
619
645
  MemoryStore::open_default()
@@ -632,16 +658,47 @@ async fn submit_tool_approval(
632
658
  state: tauri::State<'_, ApprovalsState>,
633
659
  token: String,
634
660
  approved: bool,
661
+ answer: Option<String>,
635
662
  ) -> Result<(), String> {
636
663
  let mut pending = state.pending.lock().map_err(|error| error.to_string())?;
637
664
  if let Some(tx) = pending.remove(&token) {
638
- let _ = tx.send(approved);
665
+ let outcome = if let Some(ans) = answer {
666
+ if !ans.trim().is_empty() {
667
+ ApprovalOutcome::Intercepted(ans)
668
+ } else {
669
+ ApprovalOutcome::Denied
670
+ }
671
+ } else if approved {
672
+ ApprovalOutcome::Approved
673
+ } else {
674
+ ApprovalOutcome::Denied
675
+ };
676
+ let _ = tx.send(outcome);
639
677
  Ok(())
640
678
  } else {
641
679
  Err("No pending approval found for this token".into())
642
680
  }
643
681
  }
644
682
 
683
+ #[derive(serde::Serialize)]
684
+ #[serde(rename_all = "camelCase")]
685
+ pub struct DetectedTools {
686
+ pub docker: bool,
687
+ pub git: bool,
688
+ pub gh: bool,
689
+ pub node: bool,
690
+ }
691
+
692
+ #[tauri::command]
693
+ async fn detect_system_tools() -> Result<DetectedTools, String> {
694
+ Ok(DetectedTools {
695
+ docker: mint_core::config::which("docker"),
696
+ git: mint_core::config::which("git"),
697
+ gh: mint_core::config::which("gh"),
698
+ node: mint_core::config::which("node"),
699
+ })
700
+ }
701
+
645
702
  #[derive(serde::Serialize)]
646
703
  #[serde(rename_all = "camelCase")]
647
704
  struct DesktopImageGenResponse {
@@ -860,7 +917,15 @@ async fn click_browser_selector(selector: String) -> Result<String, String> {
860
917
  )
861
918
  .await
862
919
  }
863
-
920
+ #[tauri::command]
921
+ async fn type_in_browser(selector: String, text: String) -> Result<String, String> {
922
+ mint_core::browser::type_text(
923
+ &load_config().map_err(|error| error.to_string())?,
924
+ &selector,
925
+ &text,
926
+ )
927
+ .await
928
+ }
864
929
  #[tauri::command]
865
930
  fn start_screen_capture(app: AppHandle) -> Result<(), String> {
866
931
  open_desktop_window(&app, "screen-picker")
@@ -929,6 +994,15 @@ fn reload_custom_workflows() -> Result<Value, String> {
929
994
  }))
930
995
  }
931
996
 
997
+ #[tauri::command]
998
+ fn save_custom_workflows(workflows: Vec<serde_json::Value>) -> Result<ActionResult, String> {
999
+ save_workflows(&workflows).map_err(|error| error.to_string())?;
1000
+ Ok(ActionResult {
1001
+ success: true,
1002
+ message: "workflows saved successfully".into(),
1003
+ })
1004
+ }
1005
+
932
1006
  fn install_tray(app: &AppHandle) -> tauri::Result<()> {
933
1007
  let show = MenuItem::with_id(app, "show", "Show Mint", true, None::<&str>)?;
934
1008
  let settings = MenuItem::with_id(app, "settings", "Settings", true, None::<&str>)?;
@@ -1034,7 +1108,11 @@ pub fn run() {
1034
1108
  })
1035
1109
  .invoke_handler(tauri::generate_handler![
1036
1110
  get_runtime_status,
1111
+ detect_system_tools,
1037
1112
  get_workspace_tree,
1113
+ create_workspace_file,
1114
+ create_workspace_folder,
1115
+ delete_workspace_item,
1038
1116
  generate_images,
1039
1117
  select_workspace_directory,
1040
1118
  get_config,
@@ -1054,6 +1132,9 @@ pub fn run() {
1054
1132
  get_profile_value,
1055
1133
  set_profile_value,
1056
1134
  clear_chat_history,
1135
+ list_learned_skills,
1136
+ add_learned_skill,
1137
+ delete_learned_skill,
1057
1138
  list_pictures,
1058
1139
  save_pictures,
1059
1140
  open_folder,
@@ -1076,6 +1157,7 @@ pub fn run() {
1076
1157
  navigate_browser,
1077
1158
  read_browser_page,
1078
1159
  click_browser_selector,
1160
+ type_in_browser,
1079
1161
  start_screen_capture,
1080
1162
  submit_screen_selection,
1081
1163
  submit_spotlight,
@@ -1085,7 +1167,9 @@ pub fn run() {
1085
1167
  run_next_queued_task,
1086
1168
  exit_app,
1087
1169
  open_workflows_file,
1088
- reload_custom_workflows
1170
+ reload_custom_workflows,
1171
+ save_custom_workflows,
1172
+ save_system_interaction
1089
1173
  ])
1090
1174
  .run(tauri::generate_context!())
1091
1175
  .expect("error while running Mint desktop");
@@ -5,7 +5,7 @@ use std::{
5
5
  time::{Duration, Instant},
6
6
  };
7
7
 
8
- use mint_core::{load_config, load_workflows};
8
+ use mint_core::{ChatRequest, load_config, load_workflows, send_chat};
9
9
  use serde_json::json;
10
10
  use tauri::{AppHandle, Emitter, Manager};
11
11
 
@@ -53,10 +53,62 @@ pub fn start_monitor(app: AppHandle) {
53
53
  }
54
54
  last_triggered.insert(id.into(), Instant::now());
55
55
  if let Some(main) = app.get_webview_window("main") {
56
+ let static_message = workflow["action"]["message"]
57
+ .as_str()
58
+ .unwrap_or("Automation workflow triggered");
59
+
60
+ let final_message = if let Ok(config) = load_config() {
61
+ let prompt = format!(
62
+ "You are Mint, a friendly desktop AI assistant. The user has triggered a workflow called '{}'. \
63
+ The original notification message is: '{}'. \
64
+ Please rewrite this notification message to be extremely friendly, cute, and natural. \
65
+ Write in the language matching the user's configured language: '{}'. \
66
+ Keep the length under 20 words and always end with a question (e.g. asking if they want to launch the action). \
67
+ Only reply with the rewritten message itself, do not include any quotes, markdown formatting, or extra explanations.",
68
+ workflow["name"].as_str().unwrap_or(""),
69
+ static_message,
70
+ config.language
71
+ );
72
+
73
+ let chat_req = ChatRequest {
74
+ message: prompt,
75
+ system_instruction: format!(
76
+ "You are Mint, a helpful desktop assistant. Write only the friendly suggestion in the language matching '{}'. No quotes, no markdown, no other text.",
77
+ config.language
78
+ ),
79
+ chat_id: None,
80
+ image_data_uri: None,
81
+ audio_data_uri: None,
82
+ document_attachment: None,
83
+ workspace_path: None,
84
+ };
85
+
86
+ tauri::async_runtime::block_on(async {
87
+ match send_chat(&config, &chat_req).await {
88
+ Ok(response) => {
89
+ let cleaned =
90
+ response.text.trim().trim_matches('"').to_string();
91
+ if !cleaned.is_empty() {
92
+ cleaned
93
+ } else {
94
+ static_message.to_string()
95
+ }
96
+ }
97
+ Err(err) => {
98
+ eprintln!(
99
+ "Failed to generate dynamic suggestion via AI: {:?}",
100
+ err
101
+ );
102
+ static_message.to_string()
103
+ }
104
+ }
105
+ })
106
+ } else {
107
+ static_message.to_string()
108
+ };
109
+
56
110
  let payload = json!({
57
- "message": workflow["action"]["message"]
58
- .as_str()
59
- .unwrap_or("Automation workflow triggered"),
111
+ "message": final_message,
60
112
  "suggestions": [
61
113
  { "label": "Yes, please", "action": workflow["action"] },
62
114
  { "label": "Dismiss", "action": { "type": "none" } }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Mint",
4
- "version": "1.7.1",
4
+ "version": "1.8.1",
5
5
  "identifier": "com.pheem49.mint",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run dev:desktop",