@pheem49/mint 1.8.0 → 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 (41) 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 +31 -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 +9 -0
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/config.rs +69 -1
  13. package/crates/mint-core/src/lib.rs +5 -0
  14. package/crates/mint-core/src/orchestration.rs +137 -1
  15. package/crates/mint-core/src/skills.rs +113 -4
  16. package/crates/mint-core/src/workflows.rs +1 -22
  17. package/package.json +1 -1
  18. package/src/renderer/src/components/ChatPanel.tsx +170 -33
  19. package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
  20. package/src/renderer/src/components/MintDashboard.tsx +40 -2
  21. package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
  22. package/src/renderer/src/components/SettingsWindow.tsx +1 -9
  23. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  24. package/src/renderer/src/css/image-studio.css +6 -0
  25. package/src/renderer/src/css/styles.css +64 -11
  26. package/src/renderer/src/index.css +12 -1
  27. package/src/renderer/src/tauri.ts +45 -4
  28. package/src/renderer/src-web/components/ChatPanel.tsx +163 -32
  29. package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
  30. package/src/renderer/src-web/components/MintDashboard.tsx +8 -3
  31. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
  32. package/src/renderer/src-web/components/SettingsWindow.tsx +1 -9
  33. package/src/renderer/src-web/css/image-studio.css +6 -0
  34. package/src/renderer/src-web/css/styles.css +100 -12
  35. package/src/renderer/src-web/index.css +12 -1
  36. package/src/renderer/src-web/tauri.ts +24 -2
  37. package/src-tauri/src/browser.rs +1 -141
  38. package/src-tauri/src/headless.rs +3 -2
  39. package/src-tauri/src/lib.rs +117 -26
  40. package/src-tauri/src/workflows.rs +56 -4
  41. package/src-tauri/tauri.conf.json +1 -1
@@ -1,141 +1 @@
1
- use futures_util::{SinkExt, StreamExt};
2
- use mint_core::MintConfig;
3
- use serde::Serialize;
4
- use serde_json::{Value, json};
5
- use tokio_tungstenite::{connect_async, tungstenite::Message};
6
-
7
- #[derive(Debug, Clone, Serialize)]
8
- #[serde(rename_all = "camelCase")]
9
- pub struct BrowserTab {
10
- pub id: String,
11
- pub title: String,
12
- pub url: String,
13
- }
14
-
15
- pub async fn list_tabs(config: &MintConfig) -> Result<Vec<BrowserTab>, String> {
16
- Ok(fetch_pages(config)
17
- .await?
18
- .into_iter()
19
- .filter_map(|page| {
20
- Some(BrowserTab {
21
- id: page["id"].as_str()?.to_owned(),
22
- title: page["title"].as_str().unwrap_or_default().to_owned(),
23
- url: page["url"].as_str().unwrap_or_default().to_owned(),
24
- })
25
- })
26
- .collect())
27
- }
28
-
29
- pub async fn navigate(config: &MintConfig, url: &str) -> Result<String, String> {
30
- if !(url.starts_with("https://") || url.starts_with("http://")) {
31
- return Err("browser navigation only supports http and https URLs".into());
32
- }
33
- let response = cdp_call(config, "Page.navigate", json!({ "url": url })).await?;
34
- response["result"]["frameId"]
35
- .as_str()
36
- .map(|_| format!("navigating to {url}"))
37
- .ok_or_else(|| response_error(&response))
38
- }
39
-
40
- pub async fn read_page_text(config: &MintConfig) -> Result<String, String> {
41
- let response = cdp_call(
42
- config,
43
- "Runtime.evaluate",
44
- json!({
45
- "expression": "document.body ? document.body.innerText.substring(0, 12000) : ''",
46
- "returnByValue": true
47
- }),
48
- )
49
- .await?;
50
- response["result"]["result"]["value"]
51
- .as_str()
52
- .map(str::to_owned)
53
- .ok_or_else(|| response_error(&response))
54
- }
55
-
56
- pub async fn click(config: &MintConfig, selector: &str) -> Result<String, String> {
57
- let selector = selector.trim();
58
- if selector.is_empty() || selector.len() > 500 {
59
- return Err("browser selector must contain between 1 and 500 characters".into());
60
- }
61
- let selector = serde_json::to_string(selector).map_err(|error| error.to_string())?;
62
- let response = cdp_call(
63
- config,
64
- "Runtime.evaluate",
65
- json!({
66
- "expression": format!(
67
- "(() => {{ const element = document.querySelector({selector}); if (!element) return 'not-found'; element.click(); return 'clicked'; }})()"
68
- ),
69
- "returnByValue": true
70
- }),
71
- )
72
- .await?;
73
- match response["result"]["result"]["value"].as_str() {
74
- Some("clicked") => Ok("clicked".into()),
75
- Some("not-found") => Err(format!("browser selector not found: {selector}")),
76
- _ => Err(response_error(&response)),
77
- }
78
- }
79
-
80
- async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Value, String> {
81
- let page = fetch_pages(config)
82
- .await?
83
- .into_iter()
84
- .find(|page| page["type"] == "page")
85
- .ok_or("Chrome DevTools did not report an open browser page")?;
86
- let socket_url = page["webSocketDebuggerUrl"]
87
- .as_str()
88
- .ok_or("Chrome DevTools page does not expose a websocket URL")?;
89
- let (mut socket, _) = connect_async(socket_url)
90
- .await
91
- .map_err(|error| format!("unable to connect to Chrome DevTools websocket: {error}"))?;
92
- socket
93
- .send(Message::Text(
94
- json!({ "id": 1, "method": method, "params": params })
95
- .to_string()
96
- .into(),
97
- ))
98
- .await
99
- .map_err(|error| error.to_string())?;
100
- while let Some(message) = socket.next().await {
101
- let message = message.map_err(|error| error.to_string())?;
102
- let Message::Text(raw) = message else {
103
- continue;
104
- };
105
- let value: Value = serde_json::from_str(&raw).map_err(|error| error.to_string())?;
106
- if value["id"] == 1 {
107
- return Ok(value);
108
- }
109
- }
110
- Err("Chrome DevTools websocket closed before returning a response".into())
111
- }
112
-
113
- async fn fetch_pages(config: &MintConfig) -> Result<Vec<Value>, String> {
114
- let endpoint = config
115
- .extra
116
- .get("browserDebugUrl")
117
- .and_then(Value::as_str)
118
- .unwrap_or("http://127.0.0.1:9222/json/list");
119
- let value: Value = mint_core::HTTP_CLIENT
120
- .clone()
121
- .get(endpoint)
122
- .send()
123
- .await
124
- .map_err(|error| format!("unable to reach Chrome DevTools at {endpoint}: {error}"))?
125
- .error_for_status()
126
- .map_err(|error| error.to_string())?
127
- .json()
128
- .await
129
- .map_err(|error| error.to_string())?;
130
- value
131
- .as_array()
132
- .cloned()
133
- .ok_or_else(|| "Chrome DevTools response was not a page list".into())
134
- }
135
-
136
- fn response_error(response: &Value) -> String {
137
- response["error"]["message"]
138
- .as_str()
139
- .unwrap_or("Chrome DevTools returned an unexpected response")
140
- .to_owned()
141
- }
1
+ // Deleted - Moved browser automation functions to mint-core.
@@ -8,11 +8,11 @@ use serde::Deserialize;
8
8
  use serde_json::{Value, json};
9
9
  use tauri::{AppHandle, Emitter, Manager};
10
10
 
11
- use crate::browser::{click, navigate, read_page_text};
11
+ use mint_core::browser::{click, navigate, read_page_text, type_text};
12
12
 
13
13
  const MAX_STEPS: usize = 20;
14
14
  const SYSTEM_PROMPT: &str = r#"You are Mint's native background task agent. Return only JSON:
15
- {"thought":"short progress note","action":"done|propose_folder|propose_write_file|open_url|browser_read|browser_click|knowledge_search|propose_bash","target":"path, URL, selector, query, command, or final result","data":"optional file content"}
15
+ {"thought":"short progress note","action":"done|propose_folder|propose_write_file|open_url|browser_read|browser_click|browser_type|knowledge_search|propose_bash","target":"path, URL, selector, query, command, or final result","data":"optional file content"}
16
16
  Use only one action per response. Background tasks never mutate the filesystem or execute shell commands. Use propose_folder, propose_write_file, and propose_bash so Mint can record a proposal for explicit user approval."#;
17
17
 
18
18
  #[derive(Debug, Deserialize)]
@@ -177,6 +177,7 @@ async fn execute_action(
177
177
  "open_url" => navigate(config, &action.target).await,
178
178
  "browser_read" => read_page_text(config).await,
179
179
  "browser_click" => click(config, &action.target).await,
180
+ "browser_type" => type_text(config, &action.target, &action.data).await,
180
181
  "knowledge_search" => {
181
182
  let hits = KnowledgeStore::open_default()
182
183
  .map_err(|error| error.to_string())?
@@ -1,4 +1,3 @@
1
- mod browser;
2
1
  mod desktop;
3
2
  mod discord_rpc;
4
3
  mod events;
@@ -11,7 +10,7 @@ mod updater;
11
10
  mod webhooks;
12
11
  mod workflows;
13
12
 
14
- use browser::{
13
+ use mint_core::browser::{
15
14
  BrowserTab, click as browser_click, list_tabs as browser_list_tabs,
16
15
  navigate as browser_navigate, read_page_text,
17
16
  };
@@ -41,7 +40,7 @@ use mint_core::{
41
40
  use plugins::execute_plugin;
42
41
 
43
42
  pub struct ApprovalsState {
44
- pub pending: Mutex<HashMap<String, oneshot::Sender<bool>>>,
43
+ pub pending: Mutex<HashMap<String, oneshot::Sender<ApprovalOutcome>>>,
45
44
  }
46
45
 
47
46
  static COUNTER: AtomicU64 = AtomicU64::new(1);
@@ -136,6 +135,26 @@ async fn get_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry,
136
135
  .map_err(|error| format!("workspace tree task failed: {error}"))?
137
136
  }
138
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
+
139
158
  fn build_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry, String> {
140
159
  let root = workspace_root(path.as_deref())?;
141
160
  let name = root
@@ -191,6 +210,11 @@ fn workspace_root(path: Option<&str>) -> Result<PathBuf, String> {
191
210
  None => std::env::current_dir().map_err(|error| error.to_string())?,
192
211
  };
193
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
+ };
194
218
  if !root.is_dir() {
195
219
  return Err(format!("workspace is not a directory: {}", root.display()));
196
220
  }
@@ -330,15 +354,10 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
330
354
  )
331
355
  .map_err(|e| e.to_string())?;
332
356
 
333
- let approved =
357
+ let outcome =
334
358
  tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
335
- .unwrap_or(false);
336
-
337
- if approved {
338
- Ok(ApprovalOutcome::Approved)
339
- } else {
340
- Ok(ApprovalOutcome::Denied)
341
- }
359
+ .unwrap_or(ApprovalOutcome::Denied);
360
+ Ok(outcome)
342
361
  };
343
362
 
344
363
  let progress_cb = |_| {};
@@ -413,15 +432,10 @@ async fn stream_chat_message(
413
432
  )
414
433
  .map_err(|e| e.to_string())?;
415
434
 
416
- let approved =
435
+ let outcome =
417
436
  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
- }
437
+ .unwrap_or(ApprovalOutcome::Denied);
438
+ Ok(outcome)
425
439
  };
426
440
 
427
441
  let on_progress_event = on_event.clone();
@@ -553,20 +567,52 @@ struct LearnedSkillDto {
553
567
  source_path: String,
554
568
  content: String,
555
569
  updated_at: String,
570
+ location: String,
556
571
  }
557
572
 
558
573
  #[tauri::command]
559
- fn list_learned_skills() -> Result<Vec<LearnedSkillDto>, String> {
574
+ fn list_learned_skills(workspace_path: Option<String>) -> Result<Vec<LearnedSkillDto>, String> {
560
575
  let store = MemoryStore::open_default().map_err(|e| e.to_string())?;
561
- let skills = store.learned_skills(100).map_err(|e| e.to_string())?;
562
- let dtos = skills
563
- .into_iter()
564
- .map(|s| LearnedSkillDto {
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 {
565
610
  id: s.id,
566
611
  name: s.name,
567
612
  source_path: s.source_path,
568
613
  content: s.content,
569
614
  updated_at: s.created_at,
615
+ location: loc.to_string(),
570
616
  })
571
617
  .collect();
572
618
  Ok(dtos)
@@ -584,6 +630,7 @@ fn add_learned_skill(name: String, content: String) -> Result<LearnedSkillDto, S
584
630
  source_path: skill.source_path,
585
631
  content: skill.content,
586
632
  updated_at: skill.created_at,
633
+ location: "database".to_string(),
587
634
  })
588
635
  }
589
636
 
@@ -611,16 +658,47 @@ async fn submit_tool_approval(
611
658
  state: tauri::State<'_, ApprovalsState>,
612
659
  token: String,
613
660
  approved: bool,
661
+ answer: Option<String>,
614
662
  ) -> Result<(), String> {
615
663
  let mut pending = state.pending.lock().map_err(|error| error.to_string())?;
616
664
  if let Some(tx) = pending.remove(&token) {
617
- 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);
618
677
  Ok(())
619
678
  } else {
620
679
  Err("No pending approval found for this token".into())
621
680
  }
622
681
  }
623
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
+
624
702
  #[derive(serde::Serialize)]
625
703
  #[serde(rename_all = "camelCase")]
626
704
  struct DesktopImageGenResponse {
@@ -839,7 +917,15 @@ async fn click_browser_selector(selector: String) -> Result<String, String> {
839
917
  )
840
918
  .await
841
919
  }
842
-
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
+ }
843
929
  #[tauri::command]
844
930
  fn start_screen_capture(app: AppHandle) -> Result<(), String> {
845
931
  open_desktop_window(&app, "screen-picker")
@@ -1022,7 +1108,11 @@ pub fn run() {
1022
1108
  })
1023
1109
  .invoke_handler(tauri::generate_handler![
1024
1110
  get_runtime_status,
1111
+ detect_system_tools,
1025
1112
  get_workspace_tree,
1113
+ create_workspace_file,
1114
+ create_workspace_folder,
1115
+ delete_workspace_item,
1026
1116
  generate_images,
1027
1117
  select_workspace_directory,
1028
1118
  get_config,
@@ -1067,6 +1157,7 @@ pub fn run() {
1067
1157
  navigate_browser,
1068
1158
  read_browser_page,
1069
1159
  click_browser_selector,
1160
+ type_in_browser,
1070
1161
  start_screen_capture,
1071
1162
  submit_screen_selection,
1072
1163
  submit_spotlight,
@@ -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.8.0",
4
+ "version": "1.8.1",
5
5
  "identifier": "com.pheem49.mint",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run dev:desktop",