@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.
- package/.github/workflows/release.yml +1 -1
- package/Cargo.lock +34 -3
- package/Cargo.toml +2 -2
- package/README.md +40 -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 +71 -4
- package/crates/mint-core/src/browser.rs +401 -0
- package/crates/mint-core/src/chat.rs +79 -0
- package/crates/mint-core/src/config.rs +69 -1
- package/crates/mint-core/src/lib.rs +6 -1
- 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 +9 -22
- package/package.json +2 -1
- package/src/renderer/src/App.tsx +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +197 -47
- package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
- package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
- package/src/renderer/src/components/MintDashboard.tsx +239 -7
- package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
- package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
- package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
- package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src/components/SettingsWindow.tsx +175 -970
- package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
- package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +15 -4
- package/src/renderer/src/css/settings.css +75 -6
- package/src/renderer/src/css/styles.css +419 -49
- package/src/renderer/src/css/workflows.css +542 -0
- package/src/renderer/src/env.d.ts +2 -0
- package/src/renderer/src/index.css +13 -1
- package/src/renderer/src/tauri.ts +140 -2
- package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
- package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
- package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
- package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
- package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
- package/src/renderer/src-web/css/image-studio.css +15 -4
- package/src/renderer/src-web/css/settings.css +75 -6
- package/src/renderer/src-web/css/styles.css +394 -50
- package/src/renderer/src-web/env.d.ts +2 -0
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +60 -2
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +195 -111
- package/src-tauri/src/workflows.rs +56 -4
- package/src-tauri/tauri.conf.json +1 -1
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,8 +10,7 @@ mod updater;
|
|
|
11
10
|
mod webhooks;
|
|
12
11
|
mod workflows;
|
|
13
12
|
|
|
14
|
-
use
|
|
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,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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<
|
|
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 =
|
|
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
|
|
357
|
+
let outcome =
|
|
417
358
|
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
|
-
}
|
|
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 =
|
|
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
|
|
435
|
+
let outcome =
|
|
500
436
|
tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
|
|
501
|
-
.unwrap_or(
|
|
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
|
|
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":
|
|
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" } }
|