@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
@@ -253,6 +253,16 @@ fn request_chat_id(request: &ChatRequest) -> &str {
253
253
 
254
254
  const MAX_STEPS: usize = 32;
255
255
  const MAX_OBSERVATION_BYTES: usize = 16_000;
256
+ fn is_port_9222_open() -> bool {
257
+ use std::net::TcpStream;
258
+ use std::time::Duration;
259
+ if let Ok(addr) = "127.0.0.1:9222".parse() {
260
+ TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok()
261
+ } else {
262
+ false
263
+ }
264
+ }
265
+
256
266
  pub fn build_system_prompt(config: &MintConfig) -> String {
257
267
  let mut allowed_actions = vec![
258
268
  "list_files",
@@ -284,6 +294,14 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
284
294
  "apply_patch",
285
295
  "write_file",
286
296
  ];
297
+
298
+ if is_port_9222_open() {
299
+ allowed_actions.push("browser_open");
300
+ allowed_actions.push("browser_click");
301
+ allowed_actions.push("browser_type");
302
+ allowed_actions.push("browser_read");
303
+ }
304
+
287
305
  allowed_actions.retain(|action| !config.disabled_tools.contains(&action.to_string()));
288
306
  allowed_actions.push("finish");
289
307
 
@@ -319,6 +337,19 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
319
337
  if allowed_actions.contains(&"web_search") {
320
338
  input_formats.push("- web_search: {\"query\":\"search terms\",\"limit\":5}");
321
339
  }
340
+ if allowed_actions.contains(&"browser_open") {
341
+ input_formats.push("- browser_open: {\"url\":\"https://example.com\"}");
342
+ }
343
+ if allowed_actions.contains(&"browser_click") {
344
+ input_formats.push("- browser_click: {\"selector\":\"button.submit-btn\"}");
345
+ }
346
+ if allowed_actions.contains(&"browser_type") {
347
+ input_formats
348
+ .push("- browser_type: {\"selector\":\"input.search-bar\", \"text\":\"search query\"}");
349
+ }
350
+ if allowed_actions.contains(&"browser_read") {
351
+ input_formats.push("- browser_read: {}");
352
+ }
322
353
  if allowed_actions.contains(&"memory_recall") {
323
354
  input_formats.push("- memory_recall: {\"query\":\"what did user say about X\"}");
324
355
  }
@@ -420,6 +451,20 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
420
451
  if allowed_actions.contains(&"web_search") {
421
452
  rules.push("7. Use web_search when the user asks to look something up online or needs current information.");
422
453
  }
454
+ if allowed_actions.contains(&"browser_open") {
455
+ rules.push("7a. Use browser_open to navigate the virtual browser to a URL.");
456
+ }
457
+ if allowed_actions.contains(&"browser_click") {
458
+ rules.push("7b. Use browser_click to click elements on the page.");
459
+ }
460
+ if allowed_actions.contains(&"browser_type") {
461
+ rules.push("7c. Use browser_type to enter text/input into form fields or search boxes.");
462
+ }
463
+ if allowed_actions.contains(&"browser_read") {
464
+ rules.push(
465
+ "7d. Use browser_read to read the text content of the active page to analyze it.",
466
+ );
467
+ }
423
468
  if allowed_actions.contains(&"memory_recall") {
424
469
  rules.push("8. Use memory_recall to search past interactions before asking the user to repeat context.");
425
470
  }
@@ -590,6 +635,12 @@ struct AgentInput {
590
635
  title: String,
591
636
  #[serde(default)]
592
637
  status: String,
638
+ #[serde(default)]
639
+ url: String,
640
+ #[serde(default)]
641
+ selector: String,
642
+ #[serde(default)]
643
+ text: String,
593
644
  }
594
645
 
595
646
  #[derive(Debug, Deserialize, Serialize)]
@@ -632,7 +683,7 @@ where
632
683
  ))
633
684
  })?;
634
685
  let resolved_task = resolve_github_links(task, config).await;
635
- let skills = crate::skills::learned_skills_context().unwrap_or_default();
686
+ let skills = crate::skills::learned_skills_context(Some(&root)).unwrap_or_default();
636
687
  let mut observation = initial_observation(&resolved_task, &root, &skills);
637
688
  let mut pending_image = image_data_uri;
638
689
 
@@ -1076,6 +1127,91 @@ where
1076
1127
  )),
1077
1128
  }
1078
1129
  }
1130
+ "browser_open" => {
1131
+ let url = if !input.url.is_empty() {
1132
+ &input.url
1133
+ } else if !input.path.is_empty() {
1134
+ &input.path
1135
+ } else {
1136
+ return Err(OrchestrationError::Agent(
1137
+ "browser_open requires 'url'".into(),
1138
+ ));
1139
+ };
1140
+ if crate::is_browser_running(config).await {
1141
+ let result = crate::browser::navigate(config, url)
1142
+ .await
1143
+ .map_err(|e| OrchestrationError::Agent(e))?;
1144
+ Ok(result)
1145
+ } else {
1146
+ let opened = if cfg!(target_os = "macos") {
1147
+ std::process::Command::new("open").arg(url).spawn().is_ok()
1148
+ } else if cfg!(target_os = "windows") {
1149
+ std::process::Command::new("cmd")
1150
+ .args(&["/C", "start", url])
1151
+ .spawn()
1152
+ .is_ok()
1153
+ } else {
1154
+ std::process::Command::new("xdg-open")
1155
+ .arg(url)
1156
+ .spawn()
1157
+ .is_ok()
1158
+ };
1159
+ if opened {
1160
+ Ok(format!(
1161
+ "Mint Auto is not active. Opened {url} in your default browser instead."
1162
+ ))
1163
+ } else {
1164
+ Err(OrchestrationError::Agent(
1165
+ "Failed to open URL in default browser.".into(),
1166
+ ))
1167
+ }
1168
+ }
1169
+ }
1170
+ "browser_click" => {
1171
+ let selector = if !input.selector.is_empty() {
1172
+ &input.selector
1173
+ } else if !input.path.is_empty() {
1174
+ &input.path
1175
+ } else {
1176
+ return Err(OrchestrationError::Agent(
1177
+ "browser_click requires 'selector'".into(),
1178
+ ));
1179
+ };
1180
+ let result = crate::browser::click(config, selector)
1181
+ .await
1182
+ .map_err(|e| OrchestrationError::Agent(e))?;
1183
+ Ok(result)
1184
+ }
1185
+ "browser_type" => {
1186
+ let selector = if !input.selector.is_empty() {
1187
+ &input.selector
1188
+ } else if !input.path.is_empty() {
1189
+ &input.path
1190
+ } else {
1191
+ return Err(OrchestrationError::Agent(
1192
+ "browser_type requires 'selector'".into(),
1193
+ ));
1194
+ };
1195
+ let text = if !input.text.is_empty() {
1196
+ &input.text
1197
+ } else if !input.query.is_empty() {
1198
+ &input.query
1199
+ } else {
1200
+ return Err(OrchestrationError::Agent(
1201
+ "browser_type requires 'text'".into(),
1202
+ ));
1203
+ };
1204
+ let result = crate::browser::type_text(config, selector, text)
1205
+ .await
1206
+ .map_err(|e| OrchestrationError::Agent(e))?;
1207
+ Ok(result)
1208
+ }
1209
+ "browser_read" => {
1210
+ let result = crate::browser::read_page_text(config)
1211
+ .await
1212
+ .map_err(|e| OrchestrationError::Agent(e))?;
1213
+ Ok(result)
1214
+ }
1079
1215
  "memory_recall" => {
1080
1216
  let query = required(&input.query, "query")?;
1081
1217
  let query_lower = query.to_ascii_lowercase();
@@ -53,10 +53,32 @@ pub fn learn_skill(path: &Path) -> Result<LearnedSkill, SkillError> {
53
53
  Ok(MemoryStore::open_default()?.add_learned_skill(name, &path.to_string_lossy(), &content)?)
54
54
  }
55
55
 
56
- pub fn learned_skills_context() -> Result<String, SkillError> {
57
- let skills = MemoryStore::open_default()?.learned_skills(20)?;
58
- let value = skills
59
- .into_iter()
56
+ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, SkillError> {
57
+ let mut skills = MemoryStore::open_default()?.learned_skills(20)?;
58
+
59
+ if let Some(home) = dirs::home_dir() {
60
+ let global_skills_path = home.join(".config").join("mint").join("mint-skills");
61
+ if !global_skills_path.exists() {
62
+ let _ = std::fs::create_dir_all(&global_skills_path);
63
+ }
64
+ load_skills_from_dir(&global_skills_path, &mut skills);
65
+ }
66
+
67
+ if let Some(root) = workspace_root {
68
+ let workspace_skills_path1 = root.join(".agents").join("skills");
69
+ load_skills_from_dir(&workspace_skills_path1, &mut skills);
70
+
71
+ let workspace_skills_path2 = root.join("skills");
72
+ load_skills_from_dir(&workspace_skills_path2, &mut skills);
73
+ }
74
+
75
+ let mut unique_skills = std::collections::BTreeMap::new();
76
+ for skill in skills {
77
+ unique_skills.insert(skill.name.clone(), skill);
78
+ }
79
+
80
+ let value = unique_skills
81
+ .into_values()
60
82
  .map(|skill| format!("Skill: {}\n{}", skill.name, skill.content))
61
83
  .collect::<Vec<_>>()
62
84
  .join("\n\n");
@@ -69,3 +91,90 @@ pub fn learned_skills_context() -> Result<String, SkillError> {
69
91
  }
70
92
  Ok(format!("{}\n...<learned skills truncated>", &value[..end]))
71
93
  }
94
+
95
+ pub fn load_skills_from_dir(dir: &Path, list: &mut Vec<LearnedSkill>) {
96
+ if !dir.is_dir() {
97
+ return;
98
+ }
99
+ if let Ok(entries) = fs::read_dir(dir) {
100
+ for entry in entries.flatten() {
101
+ let path = entry.path();
102
+ if path.is_dir() {
103
+ for filename in &["SKILL.md", "SKILL.txt", "skill.md", "skill.txt"] {
104
+ let skill_file = path.join(filename);
105
+ if skill_file.is_file() {
106
+ if let Ok(content) = fs::read_to_string(&skill_file) {
107
+ let name = path
108
+ .file_name()
109
+ .and_then(|n| n.to_str())
110
+ .unwrap_or("skill")
111
+ .to_string();
112
+ list.push(LearnedSkill {
113
+ id: 0,
114
+ name,
115
+ source_path: skill_file.to_string_lossy().to_string(),
116
+ content,
117
+ created_at: String::new(),
118
+ });
119
+ }
120
+ break;
121
+ }
122
+ }
123
+ } else if path.is_file() {
124
+ if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
125
+ if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
126
+ if let Ok(content) = fs::read_to_string(&path) {
127
+ let name = path
128
+ .file_stem()
129
+ .and_then(|n| n.to_str())
130
+ .unwrap_or("skill")
131
+ .to_string();
132
+ list.push(LearnedSkill {
133
+ id: 0,
134
+ name,
135
+ source_path: path.to_string_lossy().to_string(),
136
+ content,
137
+ created_at: String::new(),
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ #[cfg(test)]
148
+ mod tests {
149
+ use super::*;
150
+
151
+ #[test]
152
+ fn test_fs_skills_loading() {
153
+ let test_dir = std::env::temp_dir().join("mint_skills_test_unique_dir");
154
+ let _ = std::fs::remove_dir_all(&test_dir);
155
+ let workspace_path = test_dir.join(".agents").join("skills");
156
+ std::fs::create_dir_all(&workspace_path).unwrap();
157
+
158
+ // Write a file skill
159
+ let file_skill_path = workspace_path.join("rust-style.md");
160
+ std::fs::write(&file_skill_path, "Use 4 spaces for Rust indent.").unwrap();
161
+
162
+ // Write a directory skill
163
+ let dir_skill_path = workspace_path.join("js-style");
164
+ std::fs::create_dir_all(&dir_skill_path).unwrap();
165
+ std::fs::write(dir_skill_path.join("SKILL.md"), "Use 2 spaces for JS.").unwrap();
166
+
167
+ let mut list = Vec::new();
168
+ load_skills_from_dir(&workspace_path, &mut list);
169
+
170
+ let _ = std::fs::remove_dir_all(&test_dir);
171
+
172
+ assert_eq!(list.len(), 2);
173
+
174
+ let rust_skill = list.iter().find(|s| s.name == "rust-style").unwrap();
175
+ assert_eq!(rust_skill.content, "Use 4 spaces for Rust indent.");
176
+
177
+ let js_skill = list.iter().find(|s| s.name == "js-style").unwrap();
178
+ assert_eq!(js_skill.content, "Use 2 spaces for JS.");
179
+ }
180
+ }
@@ -51,31 +51,18 @@ pub fn load_workflows() -> Result<Vec<Value>, WorkflowError> {
51
51
  fn save_default_workflows(path: &PathBuf) -> Result<(), WorkflowError> {
52
52
  let directory = path.parent().ok_or(WorkflowError::MissingDirectory)?;
53
53
  fs::create_dir_all(directory).map_err(WorkflowError::CreateDirectory)?;
54
- let workflows = json!([
55
- {
56
- "id": "wf-1",
57
- "name": "Check Mic on Zoom",
58
- "trigger": { "type": "process_running", "processName": "zoom" },
59
- "action": {
60
- "type": "system_info",
61
- "message": "Looks like you opened Zoom. Should I check your system resources?",
62
- "target": ""
63
- }
64
- },
65
- {
66
- "id": "wf-2",
67
- "name": "Coding Time",
68
- "trigger": { "type": "process_running", "processName": "code" },
69
- "action": {
70
- "type": "open_app",
71
- "message": "Coding time. Want me to open Spotify?",
72
- "target": "spotify"
73
- }
74
- }
75
- ]);
54
+ let workflows = json!([]);
76
55
  let raw = serde_json::to_string_pretty(&workflows).map_err(WorkflowError::Serialize)?;
77
56
  fs::write(path, format!("{raw}\n")).map_err(|source| WorkflowError::Write {
78
57
  path: path.clone(),
79
58
  source,
80
59
  })
81
60
  }
61
+
62
+ pub fn save_workflows(workflows: &[Value]) -> Result<(), WorkflowError> {
63
+ let path = workflows_path()?;
64
+ let directory = path.parent().ok_or(WorkflowError::MissingDirectory)?;
65
+ fs::create_dir_all(directory).map_err(WorkflowError::CreateDirectory)?;
66
+ let raw = serde_json::to_string_pretty(workflows).map_err(WorkflowError::Serialize)?;
67
+ fs::write(&path, format!("{raw}\n")).map_err(|source| WorkflowError::Write { path, source })
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
4
  "description": "A native Tauri desktop AI assistant with a Rust backend and React UI.",
5
5
  "scripts": {
6
6
  "dev:web": "vite --config vite.config.web.ts",
@@ -58,6 +58,7 @@
58
58
  "dependencies": {
59
59
  "@tauri-apps/api": "^2.0.0",
60
60
  "@vitejs/plugin-react": "^6.0.1",
61
+ "@xyflow/react": "^12.11.1",
61
62
  "material-icon-theme": "5.34.0",
62
63
  "pixi-live2d-display": "^0.4.0",
63
64
  "pixi.js": "^6.5.10",
@@ -6,7 +6,6 @@ const WidgetWindow = lazy(() => import('./components/WidgetWindow'))
6
6
  const ProactiveGlow = lazy(() => import('./components/ProactiveGlow'))
7
7
  const ScreenPicker = lazy(() => import('./components/ScreenPicker'))
8
8
  const MintDashboard = lazy(() => import('./components/MintDashboard'))
9
-
10
9
  export default function App() {
11
10
  const [hash, setHash] = useState(window.location.hash || '#/')
12
11
 
@@ -29,5 +28,6 @@ export default function App() {
29
28
  if (route.startsWith('/proactive-glow')) content = <ProactiveGlow />
30
29
  if (route.startsWith('/screen-picker')) content = <ScreenPicker />
31
30
 
31
+
32
32
  return <Suspense fallback={null}>{content}</Suspense>
33
33
  }