@pheem49/mint 1.6.1 → 1.6.3

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 (35) hide show
  1. package/.github/workflows/ci.yml +14 -0
  2. package/Cargo.lock +3 -3
  3. package/Cargo.toml +1 -1
  4. package/bin/mint +0 -0
  5. package/package.json +11 -3
  6. package/src/renderer/index-web.html +1 -1
  7. package/src/renderer/index.html +1 -1
  8. package/src/renderer/src/components/DashboardSidebar.tsx +2 -2
  9. package/src/renderer/src/components/MintDashboard.tsx +1 -1
  10. package/src/renderer/src-web/components/ChatPanel.tsx +1 -1
  11. package/src/renderer/src-web/components/DashboardSidebar.tsx +2 -2
  12. package/src/renderer/src-web/components/MintDashboard.tsx +1 -1
  13. package/src-tauri/Cargo.toml +29 -0
  14. package/src-tauri/build.rs +3 -0
  15. package/src-tauri/capabilities/main.json +7 -0
  16. package/src-tauri/gen/schemas/acl-manifests.json +1 -0
  17. package/src-tauri/gen/schemas/capabilities.json +1 -0
  18. package/src-tauri/gen/schemas/desktop-schema.json +2412 -0
  19. package/src-tauri/gen/schemas/linux-schema.json +2412 -0
  20. package/src-tauri/src/browser.rs +141 -0
  21. package/src-tauri/src/desktop.rs +392 -0
  22. package/src-tauri/src/discord_rpc.rs +99 -0
  23. package/src-tauri/src/events.rs +70 -0
  24. package/src-tauri/src/headless.rs +222 -0
  25. package/src-tauri/src/integrations.rs +126 -0
  26. package/src-tauri/src/lib.rs +1033 -0
  27. package/src-tauri/src/main.rs +3 -0
  28. package/src-tauri/src/plugins.rs +16 -0
  29. package/src-tauri/src/proactive.rs +254 -0
  30. package/src-tauri/src/system.rs +250 -0
  31. package/src-tauri/src/updater.rs +148 -0
  32. package/src-tauri/src/webhooks.rs +255 -0
  33. package/src-tauri/src/workflows.rs +70 -0
  34. package/src-tauri/tauri.conf.json +48 -0
  35. package/tsconfig.json +1 -1
@@ -0,0 +1,1033 @@
1
+ mod browser;
2
+ mod desktop;
3
+ mod discord_rpc;
4
+ mod events;
5
+ mod headless;
6
+ mod integrations;
7
+ mod plugins;
8
+ mod proactive;
9
+ mod system;
10
+ mod updater;
11
+ mod webhooks;
12
+ mod workflows;
13
+
14
+ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
15
+ use browser::{
16
+ BrowserTab, click as browser_click, list_tabs as browser_list_tabs,
17
+ navigate as browser_navigate, read_page_text,
18
+ };
19
+
20
+ use desktop::{
21
+ ActionResult, CaptureRect, DesktopAction, capture_screen, close_window, emit_to_main,
22
+ execute_action, hide_window, integration_status, open_desktop_window, position_widget,
23
+ resize_window, translate_screen_region,
24
+ };
25
+ use events::start_system_events;
26
+ use headless::{run_next_task, start_headless_queue};
27
+ use std::collections::HashMap;
28
+ use std::sync::Mutex;
29
+ use std::sync::atomic::{AtomicU64, Ordering};
30
+ use tokio::sync::oneshot;
31
+
32
+ use integrations::{channel_inventory, list_plugins};
33
+ use mint_core::{
34
+ AgentApproval, AgentProgress, AppliedCodeEdit, ApprovalOutcome, ChatRequest, ChatResponse,
35
+ ChatSession, CodeEdit, CodeEditProposal, InteractionMemory, MemoryStore, MintConfig,
36
+ PictureEntry, TtsUrl, WeatherReport, apply_code_edits, classify_shell_command, config_path,
37
+ extract_document_text, google_tts_urls, list_saved_pictures, load_config, load_workflows,
38
+ orchestrate_agent_loop, orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
39
+ propose_code_edits, save_chat_images, save_config, start_channels, weather, workflows_path,
40
+ };
41
+ use plugins::execute_plugin;
42
+
43
+ pub struct ApprovalsState {
44
+ pub pending: Mutex<HashMap<String, oneshot::Sender<bool>>>,
45
+ }
46
+
47
+ static COUNTER: AtomicU64 = AtomicU64::new(1);
48
+ use proactive::{
49
+ record_behavior, set_enabled as set_proactive_enabled, start_loop as start_proactive_loop,
50
+ };
51
+ use serde::Serialize;
52
+ use serde_json::Value;
53
+ use std::fs;
54
+ use std::path::{Path, PathBuf};
55
+ use std::process::{Command, Stdio};
56
+ use system::{SmartContext, smart_context};
57
+ use tauri::{
58
+ AppHandle, Emitter, Manager,
59
+ ipc::Channel,
60
+ menu::{Menu, MenuItem},
61
+ tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
62
+ };
63
+ use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};
64
+ use updater::{
65
+ AvailableUpdate, UpdateChannelStatus, check as check_update, install as install_update,
66
+ status as updater_status,
67
+ };
68
+ use webhooks::start_webhooks;
69
+ use workflows::start_monitor;
70
+
71
+ #[derive(Debug, Serialize)]
72
+ #[serde(rename_all = "camelCase")]
73
+ struct RuntimeStatus {
74
+ backend: &'static str,
75
+ config_path: String,
76
+ active_provider: String,
77
+ available_providers: Vec<String>,
78
+ integrations: Value,
79
+ }
80
+
81
+ #[derive(Debug, Serialize)]
82
+ #[serde(rename_all = "camelCase")]
83
+ struct WorkspaceTreeEntry {
84
+ name: String,
85
+ path: String,
86
+ kind: &'static str,
87
+ children: Vec<WorkspaceTreeEntry>,
88
+ }
89
+
90
+ #[derive(Debug, Serialize)]
91
+ #[serde(tag = "type", rename_all = "camelCase")]
92
+ enum DesktopStreamEvent {
93
+ Chunk { chunk: String },
94
+ Progress { progress: AgentProgress },
95
+ }
96
+
97
+ const MAX_DOCUMENT_BYTES: usize = 10 * 1024 * 1024;
98
+ const MAX_DOCUMENT_CONTEXT_CHARS: usize = 30_000;
99
+ const WORKSPACE_TREE_MAX_DEPTH: usize = 9;
100
+ const WORKSPACE_TREE_MAX_CHILDREN: usize = 400;
101
+ const WORKSPACE_TREE_COLLAPSED_DIRS: &[&str] = &[
102
+ ".antigravitycli",
103
+ ".cargo_home",
104
+ ".git",
105
+ ".rustup",
106
+ ".rustup_copy",
107
+ ".rustup_home",
108
+ "build",
109
+ "coverage",
110
+ "dist",
111
+ "node_modules",
112
+ "out",
113
+ "target",
114
+ ];
115
+
116
+ fn request_with_document_context(
117
+ config: &MintConfig,
118
+ request: &ChatRequest,
119
+ ) -> Result<ChatRequest, String> {
120
+ let Some(document) = &request.document_attachment else {
121
+ return Ok(request.clone());
122
+ };
123
+
124
+ let (mime_type, encoded) = document
125
+ .data_uri
126
+ .strip_prefix("data:")
127
+ .and_then(|payload| payload.split_once(";base64,"))
128
+ .ok_or_else(|| "invalid PDF attachment data URI".to_string())?;
129
+
130
+ if !mime_type.eq_ignore_ascii_case("application/pdf") {
131
+ return Err("only PDF document attachments are supported".into());
132
+ }
133
+
134
+ let bytes = BASE64_STANDARD
135
+ .decode(encoded)
136
+ .map_err(|error| format!("invalid PDF attachment encoding: {error}"))?;
137
+ if bytes.len() > MAX_DOCUMENT_BYTES {
138
+ return Err("PDF attachment is too large (> 10 MiB)".into());
139
+ }
140
+
141
+ let directory = config_path()
142
+ .map_err(|error| error.to_string())?
143
+ .with_file_name("Attachments");
144
+ fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
145
+ let path = directory.join(format!(
146
+ "mint-pdf-{}-{}.pdf",
147
+ COUNTER.fetch_add(1, Ordering::SeqCst),
148
+ sanitize_attachment_name(&document.filename)
149
+ ));
150
+ fs::write(&path, bytes).map_err(|error| error.to_string())?;
151
+
152
+ let extracted = extract_document_text(&path, config).map_err(|error| error.to_string());
153
+ let _ = fs::remove_file(&path);
154
+ let extracted = extracted?;
155
+ let context = truncate_document_context(&extracted);
156
+ let filename = document.filename.trim();
157
+ let filename = if filename.is_empty() {
158
+ "attached.pdf"
159
+ } else {
160
+ filename
161
+ };
162
+
163
+ let mut enriched = request.clone();
164
+ enriched.document_attachment = None;
165
+ enriched.message = format!(
166
+ "{}\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 ---",
167
+ request.message
168
+ );
169
+ Ok(enriched)
170
+ }
171
+
172
+ fn truncate_document_context(text: &str) -> String {
173
+ let mut output: String = text.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
174
+ if text.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
175
+ output.push_str("\n\n[PDF text truncated because it is long.]");
176
+ }
177
+ output
178
+ }
179
+
180
+ fn sanitize_attachment_name(filename: &str) -> String {
181
+ let sanitized: String = filename
182
+ .chars()
183
+ .filter(|character| {
184
+ character.is_ascii_alphanumeric() || matches!(character, '.' | '-' | '_')
185
+ })
186
+ .take(80)
187
+ .collect();
188
+ if sanitized.is_empty() {
189
+ "document".into()
190
+ } else {
191
+ sanitized
192
+ }
193
+ }
194
+
195
+ #[tauri::command]
196
+ fn get_runtime_status() -> Result<RuntimeStatus, String> {
197
+ let config = load_config().map_err(|error| error.to_string())?;
198
+ Ok(RuntimeStatus {
199
+ backend: "rust",
200
+ config_path: config_path()
201
+ .map_err(|error| error.to_string())?
202
+ .display()
203
+ .to_string(),
204
+ active_provider: config.ai_provider.clone(),
205
+ available_providers: config
206
+ .available_providers()
207
+ .into_iter()
208
+ .map(str::to_owned)
209
+ .collect(),
210
+ integrations: integration_status(&config),
211
+ })
212
+ }
213
+
214
+ #[tauri::command]
215
+ async fn get_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry, String> {
216
+ tokio::task::spawn_blocking(move || build_workspace_tree(path))
217
+ .await
218
+ .map_err(|error| format!("workspace tree task failed: {error}"))?
219
+ }
220
+
221
+ fn build_workspace_tree(path: Option<String>) -> Result<WorkspaceTreeEntry, String> {
222
+ let root = workspace_root(path.as_deref())?;
223
+ let name = root
224
+ .file_name()
225
+ .map(|name| name.to_string_lossy().to_string())
226
+ .unwrap_or_else(|| root.display().to_string());
227
+ Ok(WorkspaceTreeEntry {
228
+ name,
229
+ path: root.display().to_string(),
230
+ kind: "directory",
231
+ children: workspace_children(&root, &root, 0)?,
232
+ })
233
+ }
234
+
235
+ #[tauri::command]
236
+ async fn select_workspace_directory() -> Result<Option<String>, String> {
237
+ tokio::task::spawn_blocking(select_workspace_directory_blocking)
238
+ .await
239
+ .map_err(|error| format!("workspace picker task failed: {error}"))?
240
+ }
241
+
242
+ fn select_workspace_directory_blocking() -> Result<Option<String>, String> {
243
+ for (program, args) in [
244
+ (
245
+ "zenity",
246
+ vec!["--file-selection", "--directory", "--title=Select Project"],
247
+ ),
248
+ ("kdialog", vec!["--getexistingdirectory", "."]),
249
+ ] {
250
+ let Ok(output) = Command::new(program)
251
+ .args(args)
252
+ .stdout(Stdio::piped())
253
+ .stderr(Stdio::null())
254
+ .output()
255
+ else {
256
+ continue;
257
+ };
258
+ if !output.status.success() {
259
+ return Ok(None);
260
+ }
261
+ let selected = String::from_utf8_lossy(&output.stdout).trim().to_string();
262
+ if selected.is_empty() {
263
+ return Ok(None);
264
+ }
265
+ return Ok(Some(workspace_root(Some(&selected))?.display().to_string()));
266
+ }
267
+ Ok(None)
268
+ }
269
+
270
+ fn workspace_root(path: Option<&str>) -> Result<PathBuf, String> {
271
+ let root = match path.map(str::trim).filter(|path| !path.is_empty()) {
272
+ Some(path) => PathBuf::from(path),
273
+ None => std::env::current_dir().map_err(|error| error.to_string())?,
274
+ };
275
+ let root = root.canonicalize().map_err(|error| error.to_string())?;
276
+ if !root.is_dir() {
277
+ return Err(format!("workspace is not a directory: {}", root.display()));
278
+ }
279
+ Ok(root)
280
+ }
281
+
282
+ fn workspace_children(
283
+ root: &Path,
284
+ directory: &Path,
285
+ depth: usize,
286
+ ) -> Result<Vec<WorkspaceTreeEntry>, String> {
287
+ if depth >= WORKSPACE_TREE_MAX_DEPTH {
288
+ return Ok(Vec::new());
289
+ }
290
+
291
+ let mut entries = fs::read_dir(directory)
292
+ .map_err(|error| error.to_string())?
293
+ .flatten()
294
+ .filter_map(|entry| {
295
+ let file_type = entry.file_type().ok()?;
296
+ if file_type.is_symlink() {
297
+ return None;
298
+ }
299
+ let name = entry.file_name().to_string_lossy().to_string();
300
+ Some((name, entry.path(), file_type.is_dir()))
301
+ })
302
+ .collect::<Vec<_>>();
303
+
304
+ entries.sort_by(|left, right| right.2.cmp(&left.2).then_with(|| left.0.cmp(&right.0)));
305
+ entries.truncate(WORKSPACE_TREE_MAX_CHILDREN);
306
+
307
+ entries
308
+ .into_iter()
309
+ .map(|(name, path, is_dir)| {
310
+ let relative = path
311
+ .strip_prefix(root)
312
+ .unwrap_or(&path)
313
+ .to_string_lossy()
314
+ .to_string();
315
+ let children = if is_dir && !WORKSPACE_TREE_COLLAPSED_DIRS.contains(&name.as_str()) {
316
+ workspace_children(root, &path, depth + 1)?
317
+ } else {
318
+ Vec::new()
319
+ };
320
+ Ok(WorkspaceTreeEntry {
321
+ name,
322
+ path: relative,
323
+ kind: if is_dir { "directory" } else { "file" },
324
+ children,
325
+ })
326
+ })
327
+ .collect()
328
+ }
329
+
330
+ #[tauri::command]
331
+ fn get_config() -> Result<MintConfig, String> {
332
+ load_config().map_err(|error| error.to_string())
333
+ }
334
+
335
+ #[tauri::command]
336
+ fn get_updater_status() -> Result<UpdateChannelStatus, String> {
337
+ Ok(updater_status(
338
+ &load_config().map_err(|error| error.to_string())?,
339
+ ))
340
+ }
341
+
342
+ #[tauri::command]
343
+ async fn check_for_updates(app: AppHandle) -> Result<AvailableUpdate, String> {
344
+ check_update(&app, &load_config().map_err(|error| error.to_string())?).await
345
+ }
346
+
347
+ #[tauri::command]
348
+ async fn install_available_update(app: AppHandle, approved: bool) -> Result<String, String> {
349
+ install_update(
350
+ &app,
351
+ &load_config().map_err(|error| error.to_string())?,
352
+ approved,
353
+ )
354
+ .await
355
+ }
356
+
357
+ #[tauri::command]
358
+ fn update_config(app: AppHandle, config: MintConfig) -> Result<(), String> {
359
+ save_config(&config).map_err(|error| error.to_string())?;
360
+ let _ = app.emit("settings-changed", &config);
361
+ if config.show_desktop_widget {
362
+ let _ = open_desktop_window(&app, "widget");
363
+ position_widget(&app);
364
+ } else if app.get_webview_window("widget").is_some() {
365
+ let _ = close_window(&app, "widget");
366
+ }
367
+ Ok(())
368
+ }
369
+
370
+ #[tauri::command]
371
+ fn inspect_shell_command(command: String) -> mint_core::ShellClassification {
372
+ classify_shell_command(&command)
373
+ }
374
+
375
+ #[tauri::command]
376
+ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatResponse, String> {
377
+ let config = load_config().map_err(|error| error.to_string())?;
378
+ let request = request_with_document_context(&config, &request)?;
379
+
380
+ if request.message.starts_with("/chat ") {
381
+ let mut clean_request = request.clone();
382
+ clean_request.message = request.message.strip_prefix("/chat ").unwrap().to_owned();
383
+
384
+ let (response, _) = orchestrate_chat_with_fallback(&config, &clean_request)
385
+ .await
386
+ .map_err(|error| error.to_string())?;
387
+ return Ok(response);
388
+ }
389
+
390
+ let root = workspace_root(request.workspace_path.as_deref())?;
391
+ let fast_mode = config
392
+ .extra
393
+ .get("enableFastMode")
394
+ .and_then(Value::as_bool)
395
+ .unwrap_or(false);
396
+
397
+ let app_clone = app.clone();
398
+ let approve_cb = move |approval: &AgentApproval| -> Result<ApprovalOutcome, String> {
399
+ let (tx, rx) = oneshot::channel();
400
+ let token = format!("tok-{}", COUNTER.fetch_add(1, Ordering::SeqCst));
401
+
402
+ let state = app_clone.state::<ApprovalsState>();
403
+ state.pending.lock().unwrap().insert(token.clone(), tx);
404
+
405
+ app_clone
406
+ .emit(
407
+ "tool-approval-requested",
408
+ serde_json::json!({
409
+ "token": token,
410
+ "approval": approval
411
+ }),
412
+ )
413
+ .map_err(|e| e.to_string())?;
414
+
415
+ let approved =
416
+ tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
417
+ .unwrap_or(false);
418
+
419
+ if approved {
420
+ Ok(ApprovalOutcome::Approved)
421
+ } else {
422
+ Ok(ApprovalOutcome::Denied)
423
+ }
424
+ };
425
+
426
+ let progress_cb = |_| {};
427
+ let on_chunk = |_| {};
428
+
429
+ let res = orchestrate_agent_loop(
430
+ &config,
431
+ &request.message,
432
+ &root,
433
+ request.image_data_uri.clone(),
434
+ request.chat_id.as_deref(),
435
+ fast_mode,
436
+ approve_cb,
437
+ progress_cb,
438
+ on_chunk,
439
+ )
440
+ .await
441
+ .map_err(|e| e.to_string())?;
442
+
443
+ Ok(ChatResponse {
444
+ provider: res.provider,
445
+ model: res.model,
446
+ text: res.summary,
447
+ fallback_provider: res.fallback,
448
+ })
449
+ }
450
+
451
+ #[tauri::command]
452
+ async fn stream_chat_message(
453
+ app: AppHandle,
454
+ request: ChatRequest,
455
+ on_event: Channel<DesktopStreamEvent>,
456
+ ) -> Result<ChatResponse, String> {
457
+ let config = load_config().map_err(|error| error.to_string())?;
458
+ let request = request_with_document_context(&config, &request)?;
459
+
460
+ if request.message.starts_with("/chat ") {
461
+ let mut clean_request = request.clone();
462
+ clean_request.message = request.message.strip_prefix("/chat ").unwrap().to_owned();
463
+
464
+ let (response, _) =
465
+ orchestrate_chat_stream_with_fallback(&config, &clean_request, |chunk| {
466
+ let _ = on_event.send(DesktopStreamEvent::Chunk { chunk });
467
+ })
468
+ .await
469
+ .map_err(|error| error.to_string())?;
470
+ return Ok(response);
471
+ }
472
+
473
+ let root = workspace_root(request.workspace_path.as_deref())?;
474
+ let fast_mode = config
475
+ .extra
476
+ .get("enableFastMode")
477
+ .and_then(Value::as_bool)
478
+ .unwrap_or(false);
479
+
480
+ let app_clone = app.clone();
481
+ let approve_cb = move |approval: &AgentApproval| -> Result<ApprovalOutcome, String> {
482
+ let (tx, rx) = oneshot::channel();
483
+ let token = format!("tok-{}", COUNTER.fetch_add(1, Ordering::SeqCst));
484
+
485
+ let state = app_clone.state::<ApprovalsState>();
486
+ state.pending.lock().unwrap().insert(token.clone(), tx);
487
+
488
+ app_clone
489
+ .emit(
490
+ "tool-approval-requested",
491
+ serde_json::json!({
492
+ "token": token,
493
+ "approval": approval
494
+ }),
495
+ )
496
+ .map_err(|e| e.to_string())?;
497
+
498
+ let approved =
499
+ tokio::task::block_in_place(move || tokio::runtime::Handle::current().block_on(rx))
500
+ .unwrap_or(false);
501
+
502
+ if approved {
503
+ Ok(ApprovalOutcome::Approved)
504
+ } else {
505
+ Ok(ApprovalOutcome::Denied)
506
+ }
507
+ };
508
+
509
+ let on_progress_event = on_event.clone();
510
+ let progress_cb = move |progress| {
511
+ let _ = on_progress_event.send(DesktopStreamEvent::Progress { progress });
512
+ };
513
+
514
+ let on_event_clone = on_event.clone();
515
+ let on_chunk = move |summary: String| {
516
+ let chars: Vec<char> = summary.chars().collect();
517
+ let mut i = 0;
518
+ while i < chars.len() {
519
+ let end = (i + 4).min(chars.len());
520
+ let chunk: String = chars[i..end].iter().collect();
521
+ let _ = on_event_clone.send(DesktopStreamEvent::Chunk { chunk });
522
+ i = end;
523
+ std::thread::sleep(std::time::Duration::from_millis(15));
524
+ }
525
+ };
526
+
527
+ let res = orchestrate_agent_loop(
528
+ &config,
529
+ &request.message,
530
+ &root,
531
+ request.image_data_uri.clone(),
532
+ request.chat_id.as_deref(),
533
+ fast_mode,
534
+ approve_cb,
535
+ progress_cb,
536
+ on_chunk,
537
+ )
538
+ .await
539
+ .map_err(|e| e.to_string())?;
540
+
541
+ Ok(ChatResponse {
542
+ provider: res.provider,
543
+ model: res.model,
544
+ text: res.summary,
545
+ fallback_provider: res.fallback,
546
+ })
547
+ }
548
+
549
+ #[tauri::command]
550
+ fn get_recent_interactions(
551
+ limit: Option<usize>,
552
+ chat_id: Option<String>,
553
+ ) -> Result<Vec<InteractionMemory>, String> {
554
+ MemoryStore::open_default()
555
+ .and_then(|memory| {
556
+ memory.recent_interactions_for_chat(
557
+ chat_id
558
+ .as_deref()
559
+ .unwrap_or(mint_core::DEFAULT_CONVERSATION_ID),
560
+ limit.unwrap_or(5),
561
+ )
562
+ })
563
+ .map_err(|error| error.to_string())
564
+ }
565
+
566
+ #[tauri::command]
567
+ fn list_chat_sessions() -> Result<Vec<ChatSession>, String> {
568
+ MemoryStore::open_default()
569
+ .and_then(|memory| memory.list_chat_sessions())
570
+ .map_err(|error| error.to_string())
571
+ }
572
+
573
+ #[tauri::command]
574
+ fn delete_chat_session(chat_id: String) -> Result<usize, String> {
575
+ MemoryStore::open_default()
576
+ .and_then(|memory| memory.delete_chat_session(&chat_id))
577
+ .map_err(|error| error.to_string())
578
+ }
579
+
580
+ #[tauri::command]
581
+ fn rename_chat_session(chat_id: String, new_title: String) -> Result<usize, String> {
582
+ MemoryStore::open_default()
583
+ .and_then(|memory| memory.rename_chat_session(&chat_id, &new_title))
584
+ .map_err(|error| error.to_string())
585
+ }
586
+
587
+ #[tauri::command]
588
+ fn get_profile_value(key: String) -> Result<Option<String>, String> {
589
+ MemoryStore::open_default()
590
+ .and_then(|memory| memory.get_profile(&key))
591
+ .map_err(|error| error.to_string())
592
+ }
593
+
594
+ #[tauri::command]
595
+ fn set_profile_value(key: String, value: String) -> Result<(), String> {
596
+ MemoryStore::open_default()
597
+ .and_then(|memory| memory.set_profile(&key, &value))
598
+ .map_err(|error| error.to_string())
599
+ }
600
+
601
+ #[tauri::command]
602
+ fn clear_chat_history(chat_id: Option<String>) -> Result<usize, String> {
603
+ MemoryStore::open_default()
604
+ .and_then(|memory| {
605
+ memory.clear_interactions_for_chat(
606
+ chat_id
607
+ .as_deref()
608
+ .unwrap_or(mint_core::DEFAULT_CONVERSATION_ID),
609
+ )
610
+ })
611
+ .map_err(|error| error.to_string())
612
+ }
613
+
614
+ #[tauri::command]
615
+ async fn submit_tool_approval(
616
+ state: tauri::State<'_, ApprovalsState>,
617
+ token: String,
618
+ approved: bool,
619
+ ) -> Result<(), String> {
620
+ let mut pending = state.pending.lock().map_err(|error| error.to_string())?;
621
+ if let Some(tx) = pending.remove(&token) {
622
+ let _ = tx.send(approved);
623
+ Ok(())
624
+ } else {
625
+ Err("No pending approval found for this token".into())
626
+ }
627
+ }
628
+
629
+ #[tauri::command]
630
+ fn list_pictures() -> Result<Vec<PictureEntry>, String> {
631
+ list_saved_pictures().map_err(|error| error.to_string())
632
+ }
633
+
634
+ #[tauri::command]
635
+ fn save_pictures(
636
+ images: Vec<String>,
637
+ source: Option<String>,
638
+ message: Option<String>,
639
+ ) -> Result<Vec<PictureEntry>, String> {
640
+ save_chat_images(images, source, message).map_err(|error| error.to_string())
641
+ }
642
+
643
+ #[tauri::command]
644
+ fn open_folder(path: String) -> Result<ActionResult, String> {
645
+ let target_path = PathBuf::from(path.trim());
646
+ if target_path.as_os_str().is_empty() {
647
+ return Err("folder path is required".into());
648
+ }
649
+
650
+ let folder = if target_path.is_dir() {
651
+ target_path
652
+ } else {
653
+ target_path
654
+ .parent()
655
+ .map(PathBuf::from)
656
+ .ok_or_else(|| "could not resolve containing folder".to_string())?
657
+ };
658
+
659
+ Command::new("xdg-open")
660
+ .arg(&folder)
661
+ .spawn()
662
+ .map_err(|error| error.to_string())?;
663
+ Ok(ActionResult {
664
+ success: true,
665
+ message: format!("opened {}", folder.display()),
666
+ })
667
+ }
668
+
669
+ #[tauri::command]
670
+ fn get_tts_urls(text: String) -> Result<Vec<TtsUrl>, String> {
671
+ let language = load_config().map_err(|error| error.to_string())?.language;
672
+ Ok(google_tts_urls(&text, &language))
673
+ }
674
+
675
+ #[tauri::command]
676
+ async fn get_weather(city: String) -> Result<WeatherReport, String> {
677
+ weather(&city).await.map_err(|error| error.to_string())
678
+ }
679
+
680
+ #[tauri::command]
681
+ fn propose_desktop_code_edits(
682
+ root: String,
683
+ edits: Vec<CodeEdit>,
684
+ ) -> Result<CodeEditProposal, String> {
685
+ propose_code_edits(
686
+ std::path::Path::new(&root),
687
+ &edits,
688
+ &load_config().map_err(|error| error.to_string())?,
689
+ )
690
+ .map_err(|error| error.to_string())
691
+ }
692
+
693
+ #[tauri::command]
694
+ fn apply_desktop_code_edits(
695
+ root: String,
696
+ edits: Vec<CodeEdit>,
697
+ approval_token: String,
698
+ ) -> Result<Vec<AppliedCodeEdit>, String> {
699
+ apply_code_edits(
700
+ std::path::Path::new(&root),
701
+ &edits,
702
+ &approval_token,
703
+ &load_config().map_err(|error| error.to_string())?,
704
+ )
705
+ .map_err(|error| error.to_string())
706
+ }
707
+
708
+ #[tauri::command]
709
+ fn open_window(app: AppHandle, kind: String) -> Result<(), String> {
710
+ open_desktop_window(&app, &kind)?;
711
+ if kind == "widget" {
712
+ position_widget(&app);
713
+ }
714
+ Ok(())
715
+ }
716
+
717
+ #[tauri::command]
718
+ fn hide_desktop_window(app: AppHandle, label: String) -> Result<(), String> {
719
+ hide_window(&app, &label)
720
+ }
721
+
722
+ #[tauri::command]
723
+ fn close_desktop_window(app: AppHandle, label: String) -> Result<(), String> {
724
+ close_window(&app, &label)
725
+ }
726
+
727
+ #[tauri::command]
728
+ fn resize_desktop_window(
729
+ app: AppHandle,
730
+ label: String,
731
+ width: u32,
732
+ height: u32,
733
+ ) -> Result<(), String> {
734
+ resize_window(&app, &label, width, height)
735
+ }
736
+
737
+ #[tauri::command]
738
+ fn run_desktop_action(action: DesktopAction) -> Result<ActionResult, String> {
739
+ let config = load_config().map_err(|error| error.to_string())?;
740
+ execute_action(&config, action)
741
+ }
742
+
743
+ #[tauri::command]
744
+ fn get_integration_inventory() -> Result<Value, String> {
745
+ let config = load_config().map_err(|error| error.to_string())?;
746
+ Ok(serde_json::json!({
747
+ "mcpServers": mint_core::configured_mcp_servers(&config)
748
+ .map_err(|error| error.to_string())?
749
+ .keys()
750
+ .collect::<Vec<_>>(),
751
+ "plugins": list_plugins(&config),
752
+ "channels": channel_inventory(&config)
753
+ }))
754
+ }
755
+
756
+ #[tauri::command]
757
+ async fn run_native_plugin(name: String, instruction: String) -> Result<String, String> {
758
+ let config = load_config().map_err(|error| error.to_string())?;
759
+ execute_plugin(&config, &name, &instruction).await
760
+ }
761
+
762
+ #[tauri::command]
763
+ fn capture_silent_screen() -> Result<String, String> {
764
+ capture_screen()
765
+ }
766
+
767
+ #[tauri::command]
768
+ fn read_clipboard_image() -> Result<String, String> {
769
+ desktop::read_clipboard_image()
770
+ }
771
+
772
+ #[tauri::command]
773
+ async fn translate_capture_region(rect: CaptureRect) -> Result<String, String> {
774
+ let config = load_config().map_err(|error| error.to_string())?;
775
+ translate_screen_region(&config, rect).await
776
+ }
777
+
778
+ #[tauri::command]
779
+ async fn get_smart_context() -> SmartContext {
780
+ smart_context().await
781
+ }
782
+
783
+ #[tauri::command]
784
+ async fn get_browser_tabs() -> Result<Vec<BrowserTab>, String> {
785
+ browser_list_tabs(&load_config().map_err(|error| error.to_string())?).await
786
+ }
787
+
788
+ #[tauri::command]
789
+ async fn navigate_browser(url: String) -> Result<String, String> {
790
+ browser_navigate(&load_config().map_err(|error| error.to_string())?, &url).await
791
+ }
792
+
793
+ #[tauri::command]
794
+ async fn read_browser_page() -> Result<String, String> {
795
+ read_page_text(&load_config().map_err(|error| error.to_string())?).await
796
+ }
797
+
798
+ #[tauri::command]
799
+ async fn click_browser_selector(selector: String) -> Result<String, String> {
800
+ browser_click(
801
+ &load_config().map_err(|error| error.to_string())?,
802
+ &selector,
803
+ )
804
+ .await
805
+ }
806
+
807
+ #[tauri::command]
808
+ fn start_screen_capture(app: AppHandle) -> Result<(), String> {
809
+ open_desktop_window(&app, "screen-picker")
810
+ }
811
+
812
+ #[tauri::command]
813
+ fn submit_screen_selection(app: AppHandle, image: String) {
814
+ emit_to_main(&app, "vision-ready", image);
815
+ let _ = close_window(&app, "screen-picker");
816
+ }
817
+
818
+ #[tauri::command]
819
+ fn submit_spotlight(app: AppHandle, query: String) {
820
+ emit_to_main(&app, "spotlight-to-chat", query);
821
+ let _ = hide_window(&app, "spotlight");
822
+ }
823
+
824
+ #[tauri::command]
825
+ fn set_ai_state(app: AppHandle, state: String) {
826
+ if let Some(widget) = app.get_webview_window("widget") {
827
+ let _ = widget.emit("widget-state", state);
828
+ }
829
+ }
830
+
831
+ #[tauri::command]
832
+ fn toggle_proactive(enabled: bool) {
833
+ set_proactive_enabled(enabled);
834
+ }
835
+
836
+ #[tauri::command]
837
+ fn save_behavior_context(context: String) -> Result<(), String> {
838
+ record_behavior(&context)
839
+ }
840
+
841
+ #[tauri::command]
842
+ async fn run_next_queued_task(app: AppHandle) -> Result<Option<mint_core::Task>, String> {
843
+ run_next_task(&app).await
844
+ }
845
+
846
+ #[tauri::command]
847
+ fn exit_app(app: AppHandle) {
848
+ app.exit(0);
849
+ }
850
+
851
+ #[tauri::command]
852
+ fn open_workflows_file() -> Result<ActionResult, String> {
853
+ load_workflows().map_err(|error| error.to_string())?;
854
+ let path = workflows_path().map_err(|error| error.to_string())?;
855
+ Command::new("xdg-open")
856
+ .arg(path)
857
+ .spawn()
858
+ .map_err(|error| error.to_string())?;
859
+ Ok(ActionResult {
860
+ success: true,
861
+ message: "opened workflows file".into(),
862
+ })
863
+ }
864
+
865
+ #[tauri::command]
866
+ fn reload_custom_workflows() -> Result<Value, String> {
867
+ let workflows = load_workflows().map_err(|error| error.to_string())?;
868
+ Ok(serde_json::json!({
869
+ "success": true,
870
+ "count": workflows.len(),
871
+ "workflows": workflows
872
+ }))
873
+ }
874
+
875
+ fn install_tray(app: &AppHandle) -> tauri::Result<()> {
876
+ let show = MenuItem::with_id(app, "show", "Show Mint", true, None::<&str>)?;
877
+ let settings = MenuItem::with_id(app, "settings", "Settings", true, None::<&str>)?;
878
+ let spotlight = MenuItem::with_id(app, "spotlight", "Spotlight", true, None::<&str>)?;
879
+ let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
880
+ let menu = Menu::with_items(app, &[&show, &settings, &spotlight, &quit])?;
881
+ let mut builder = TrayIconBuilder::new()
882
+ .tooltip("Mint AI Assistant")
883
+ .menu(&menu)
884
+ .on_menu_event(|app, event| match event.id().as_ref() {
885
+ "show" => {
886
+ let _ = app.get_webview_window("main").map(|window| window.show());
887
+ }
888
+ "settings" => {
889
+ let _ = open_desktop_window(app, "settings");
890
+ }
891
+ "spotlight" => {
892
+ let _ = open_desktop_window(app, "spotlight");
893
+ }
894
+ "quit" => app.exit(0),
895
+ _ => {}
896
+ })
897
+ .on_tray_icon_event(|tray, event| {
898
+ if matches!(
899
+ event,
900
+ TrayIconEvent::Click {
901
+ button: MouseButton::Left,
902
+ button_state: MouseButtonState::Up,
903
+ ..
904
+ }
905
+ ) {
906
+ let app = tray.app_handle();
907
+ if let Some(window) = app.get_webview_window("main") {
908
+ let _ = if window.is_visible().unwrap_or(false) {
909
+ window.hide()
910
+ } else {
911
+ window.show()
912
+ };
913
+ }
914
+ }
915
+ });
916
+ if let Some(icon) = app.default_window_icon() {
917
+ builder = builder.icon(icon.clone());
918
+ }
919
+ builder.build(app)?;
920
+ Ok(())
921
+ }
922
+
923
+ fn install_shortcuts(app: &AppHandle) -> tauri::Result<()> {
924
+ let main_shortcut = Shortcut::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::Space);
925
+ let spotlight_shortcut = Shortcut::new(Some(Modifiers::ALT), Code::Space);
926
+ let main_handler = main_shortcut.clone();
927
+ let spotlight_handler = spotlight_shortcut.clone();
928
+ app.plugin(
929
+ tauri_plugin_global_shortcut::Builder::new()
930
+ .with_handler(move |app, shortcut, event| {
931
+ if event.state() != ShortcutState::Pressed {
932
+ return;
933
+ }
934
+ if shortcut == &main_handler {
935
+ if let Some(window) = app.get_webview_window("main") {
936
+ let _ = if window.is_visible().unwrap_or(false) {
937
+ window.hide()
938
+ } else {
939
+ window.show()
940
+ };
941
+ }
942
+ } else if shortcut == &spotlight_handler {
943
+ let _ = open_desktop_window(app, "spotlight");
944
+ }
945
+ })
946
+ .build(),
947
+ )?;
948
+ let _ = app.global_shortcut().register(main_shortcut);
949
+ let _ = app.global_shortcut().register(spotlight_shortcut);
950
+ Ok(())
951
+ }
952
+
953
+ #[cfg_attr(mobile, tauri::mobile_entry_point)]
954
+ pub fn run() {
955
+ tauri::Builder::default()
956
+ .plugin(tauri_plugin_updater::Builder::new().build())
957
+ .manage(ApprovalsState {
958
+ pending: Mutex::new(HashMap::new()),
959
+ })
960
+ .setup(|app| {
961
+ install_tray(app.handle())?;
962
+ install_shortcuts(app.handle())?;
963
+ start_monitor(app.handle().clone());
964
+ start_system_events(app.handle().clone());
965
+ start_headless_queue(app.handle().clone());
966
+ start_proactive_loop(app.handle().clone());
967
+ start_channels();
968
+ start_webhooks();
969
+ if load_config()
970
+ .map(|config| config.show_desktop_widget)
971
+ .unwrap_or(false)
972
+ {
973
+ let _ = open_desktop_window(app.handle(), "widget");
974
+ position_widget(app.handle());
975
+ }
976
+ Ok(())
977
+ })
978
+ .invoke_handler(tauri::generate_handler![
979
+ get_runtime_status,
980
+ get_workspace_tree,
981
+ select_workspace_directory,
982
+ get_config,
983
+ get_updater_status,
984
+ check_for_updates,
985
+ install_available_update,
986
+ update_config,
987
+ inspect_shell_command,
988
+ send_chat_message,
989
+ stream_chat_message,
990
+ submit_tool_approval,
991
+ get_recent_interactions,
992
+ list_chat_sessions,
993
+ delete_chat_session,
994
+ rename_chat_session,
995
+ get_profile_value,
996
+ set_profile_value,
997
+ clear_chat_history,
998
+ list_pictures,
999
+ save_pictures,
1000
+ open_folder,
1001
+ get_tts_urls,
1002
+ get_weather,
1003
+ propose_desktop_code_edits,
1004
+ apply_desktop_code_edits,
1005
+ open_window,
1006
+ hide_desktop_window,
1007
+ close_desktop_window,
1008
+ resize_desktop_window,
1009
+ run_desktop_action,
1010
+ get_integration_inventory,
1011
+ run_native_plugin,
1012
+ capture_silent_screen,
1013
+ read_clipboard_image,
1014
+ translate_capture_region,
1015
+ get_smart_context,
1016
+ get_browser_tabs,
1017
+ navigate_browser,
1018
+ read_browser_page,
1019
+ click_browser_selector,
1020
+ start_screen_capture,
1021
+ submit_screen_selection,
1022
+ submit_spotlight,
1023
+ set_ai_state,
1024
+ toggle_proactive,
1025
+ save_behavior_context,
1026
+ run_next_queued_task,
1027
+ exit_app,
1028
+ open_workflows_file,
1029
+ reload_custom_workflows
1030
+ ])
1031
+ .run(tauri::generate_context!())
1032
+ .expect("error while running Mint desktop");
1033
+ }