@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.
- package/.github/workflows/release.yml +1 -1
- package/Cargo.lock +34 -3
- package/Cargo.toml +2 -2
- package/README.md +31 -0
- package/Release_Note.md +95 -0
- package/assets/Mint_CLI.png +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/agent.rs +142 -48
- package/crates/mint-cli/src/main.rs +231 -22
- package/crates/mint-core/src/api_server.rs +9 -0
- package/crates/mint-core/src/browser.rs +401 -0
- package/crates/mint-core/src/config.rs +69 -1
- package/crates/mint-core/src/lib.rs +5 -0
- package/crates/mint-core/src/orchestration.rs +137 -1
- package/crates/mint-core/src/skills.rs +113 -4
- package/crates/mint-core/src/workflows.rs +1 -22
- package/package.json +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +170 -33
- package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src/components/MintDashboard.tsx +40 -2
- package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src/components/SettingsWindow.tsx +1 -9
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +6 -0
- package/src/renderer/src/css/styles.css +64 -11
- package/src/renderer/src/index.css +12 -1
- package/src/renderer/src/tauri.ts +45 -4
- package/src/renderer/src-web/components/ChatPanel.tsx +163 -32
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +8 -3
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src-web/components/SettingsWindow.tsx +1 -9
- package/src/renderer/src-web/css/image-studio.css +6 -0
- package/src/renderer/src-web/css/styles.css +100 -12
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +24 -2
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +117 -26
- package/src-tauri/src/workflows.rs +56 -4
- package/src-tauri/tauri.conf.json +1 -1
package/src-tauri/src/browser.rs
CHANGED
|
@@ -1,141 +1 @@
|
|
|
1
|
-
|
|
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
|
|
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())?
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -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<
|
|
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
|
|
357
|
+
let outcome =
|
|
334
358
|
tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
|
|
335
|
-
.unwrap_or(
|
|
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
|
|
435
|
+
let outcome =
|
|
417
436
|
tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
|
|
418
|
-
.unwrap_or(
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
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":
|
|
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" } }
|