@pheem49/mint 1.9.0 → 1.10.0

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 (90) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +285 -140
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +287 -43
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/interactive.rs +2888 -0
  10. package/crates/mint-cli/src/main.rs +336 -2854
  11. package/crates/mint-cli/src/markdown.rs +294 -0
  12. package/crates/mint-cli/src/onboard.rs +44 -1
  13. package/crates/mint-cli/src/updater.rs +48 -6
  14. package/crates/mint-core/Cargo.toml +1 -0
  15. package/crates/mint-core/src/agent_loop.rs +1 -0
  16. package/crates/mint-core/src/api_server.rs +376 -13
  17. package/crates/mint-core/src/browser.rs +629 -101
  18. package/crates/mint-core/src/channels.rs +105 -6
  19. package/crates/mint-core/src/chat.rs +52 -4
  20. package/crates/mint-core/src/code_tools.rs +29 -11
  21. package/crates/mint-core/src/config.rs +118 -0
  22. package/crates/mint-core/src/image_gen.rs +294 -33
  23. package/crates/mint-core/src/lib.rs +5 -2
  24. package/crates/mint-core/src/mcp.rs +119 -6
  25. package/crates/mint-core/src/orchestration.rs +133 -11
  26. package/crates/mint-core/src/pictures.rs +64 -1
  27. package/crates/mint-core/src/skills.rs +82 -6
  28. package/crates/mint-core/src/video_gen.rs +235 -0
  29. package/package.json +8 -2
  30. package/shared-modules/luajit/luajit.json +2 -2
  31. package/skills-lock.json +41 -0
  32. package/src/renderer/index-web.html +1 -1
  33. package/src/renderer/index.html +1 -1
  34. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  35. package/src/renderer/shared/platform.ts +3 -0
  36. package/src/renderer/shared/types.ts +4 -0
  37. package/src/renderer/shared/utils/approval.tsx +86 -7
  38. package/src/renderer/shared/utils/markdown.tsx +217 -88
  39. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  40. package/src/renderer/src/components/ChatPanel.tsx +461 -64
  41. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  42. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  43. package/src/renderer/src/components/MintDashboard.tsx +150 -5
  44. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  45. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  46. package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
  47. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  48. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  49. package/src/renderer/src/components/SettingsWindow.tsx +38 -2
  50. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  51. package/src/renderer/src/css/image-studio.css +106 -0
  52. package/src/renderer/src/css/settings.css +158 -0
  53. package/src/renderer/src/css/styles.css +352 -1
  54. package/src/renderer/src/css/veo-studio.css +626 -0
  55. package/src/renderer/src/index.css +1 -0
  56. package/src/renderer/src/tauri.ts +144 -17
  57. package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
  58. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  59. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  60. package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
  61. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  62. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  63. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
  64. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  65. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  66. package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
  67. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  68. package/src/renderer/src-web/css/image-studio.css +106 -0
  69. package/src/renderer/src-web/css/settings.css +158 -0
  70. package/src/renderer/src-web/css/styles.css +353 -1
  71. package/src/renderer/src-web/css/veo-studio.css +650 -0
  72. package/src/renderer/src-web/index.css +1 -0
  73. package/src/renderer/src-web/tauri.ts +135 -16
  74. package/src-tauri/src/lib.rs +114 -8
  75. package/src-tauri/src/webhooks.rs +45 -31
  76. package/src-tauri/src/workflows.rs +1 -0
  77. package/src-tauri/tauri.conf.json +1 -1
  78. package/temp_gridgeist/AGENTS.md +8 -0
  79. package/temp_gridgeist/CHANGELOG.md +130 -0
  80. package/temp_gridgeist/LICENSE +21 -0
  81. package/temp_gridgeist/README.md +323 -0
  82. package/temp_gridgeist/README.th.md +325 -0
  83. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  84. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  85. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  86. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  87. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  88. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  89. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  90. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -61,7 +61,24 @@ async fn telegram_loop() -> Result<(), String> {
61
61
  ) else {
62
62
  continue;
63
63
  };
64
- let answer = answer(text, "Reply concisely for a Telegram chat.").await;
64
+ let formatted_chat_id = format!("telegram:{chat_id}");
65
+ if let Ok(config) = load_config() {
66
+ if config.bridge_ack_enabled() {
67
+ let _ = client
68
+ .post(format!(
69
+ "https://api.telegram.org/bot{token}/sendChatAction"
70
+ ))
71
+ .json(&json!({ "chat_id": chat_id, "action": "typing" }))
72
+ .send()
73
+ .await;
74
+ }
75
+ }
76
+ let answer = answer_channel(
77
+ text,
78
+ "Reply concisely for a Telegram chat.",
79
+ Some(formatted_chat_id),
80
+ )
81
+ .await;
65
82
  let _ = client
66
83
  .post(format!("https://api.telegram.org/bot{token}/sendMessage"))
67
84
  .json(&json!({ "chat_id": chat_id, "text": answer }))
@@ -116,7 +133,14 @@ async fn discord_loop() -> Result<(), String> {
116
133
  mentions.iter().any(|mention| mention["id"].as_str() == Some(&bot_id))
117
134
  });
118
135
  if !direct_message && !mentioned { continue }
119
- let reply = answer(text, "Reply concisely for a Discord chat.").await;
136
+ if let Ok(config) = load_config() {
137
+ if config.bridge_ack_enabled() {
138
+ let _ = crate::HTTP_CLIENT.clone().post(format!("https://discord.com/api/v10/channels/{channel}/typing"))
139
+ .header("Authorization", format!("Bot {token}")).send().await;
140
+ }
141
+ }
142
+ let formatted_chat_id = format!("discord:{channel}");
143
+ let reply = answer_channel(text, "Reply concisely for a Discord chat.", Some(formatted_chat_id)).await;
120
144
  let _ = crate::HTTP_CLIENT.clone().post(format!("https://discord.com/api/v10/channels/{channel}/messages"))
121
145
  .header("Authorization", format!("Bot {token}")).json(&json!({ "content": reply })).send().await;
122
146
  }
@@ -168,7 +192,13 @@ async fn slack_loop() -> Result<(), String> {
168
192
  else {
169
193
  continue;
170
194
  };
171
- let reply = answer(text, "Reply concisely for a Slack chat.").await;
195
+ let formatted_chat_id = format!("slack:{channel}");
196
+ let reply = answer_channel(
197
+ text,
198
+ "Reply concisely for a Slack chat.",
199
+ Some(formatted_chat_id),
200
+ )
201
+ .await;
172
202
  let _ = crate::HTTP_CLIENT
173
203
  .clone()
174
204
  .post("https://slack.com/api/chat.postMessage")
@@ -180,20 +210,89 @@ async fn slack_loop() -> Result<(), String> {
180
210
  Ok(())
181
211
  }
182
212
 
183
- async fn answer(text: &str, system_instruction: &str) -> String {
213
+ pub fn is_action_intent(text: &str) -> bool {
214
+ let lower = text.to_lowercase();
215
+ let action_keywords = [
216
+ "แก้",
217
+ "สร้าง",
218
+ "ลบ",
219
+ "รัน",
220
+ "ตรวจ",
221
+ "ค้นหา",
222
+ "เช็ค",
223
+ "ดูไฟล์",
224
+ "อ่านไฟล์",
225
+ "วิเคราะห์ไฟล์",
226
+ "fix",
227
+ "create",
228
+ "build",
229
+ "run",
230
+ "delete",
231
+ "edit",
232
+ "update",
233
+ "check",
234
+ "find",
235
+ "search",
236
+ "read file",
237
+ "git",
238
+ "patch",
239
+ "test",
240
+ "analyze file",
241
+ ];
242
+ action_keywords
243
+ .iter()
244
+ .any(|&keyword| lower.contains(keyword))
245
+ }
246
+
247
+ pub async fn answer_channel(
248
+ text: &str,
249
+ system_instruction: &str,
250
+ chat_id: Option<String>,
251
+ ) -> String {
184
252
  let Ok(config) = load_config() else {
185
253
  return "Mint config error".into();
186
254
  };
255
+ let workspace = config.active_workspace_path();
256
+ let workspace_str = workspace.as_ref().map(|p| p.to_string_lossy().to_string());
257
+
258
+ if is_action_intent(text)
259
+ && let Some(ref root_path) = workspace
260
+ {
261
+ if root_path.exists() {
262
+ let agent_result = crate::orchestrate_agent_loop(
263
+ &config,
264
+ text,
265
+ root_path,
266
+ None,
267
+ None,
268
+ chat_id.as_deref(),
269
+ None,
270
+ true,
271
+ |_approval| Ok(crate::ApprovalOutcome::Approved),
272
+ |_progress| {},
273
+ |_chunk| {},
274
+ )
275
+ .await;
276
+
277
+ if let Ok(res) = agent_result {
278
+ if !res.summary.trim().is_empty() {
279
+ return res.summary;
280
+ }
281
+ }
282
+ }
283
+ }
284
+
187
285
  orchestrate_chat(
188
286
  &config,
189
287
  &ChatRequest {
190
288
  message: text.into(),
191
289
  system_instruction: system_instruction.into(),
192
- chat_id: None,
290
+ chat_id,
193
291
  image_data_uri: None,
194
292
  audio_data_uri: None,
293
+ video_data_uri: None,
195
294
  document_attachment: None,
196
- workspace_path: None,
295
+ workspace_path: workspace_str,
197
296
  agent_id: None,
198
297
  },
199
298
  )
@@ -96,6 +96,8 @@ pub struct ChatRequest {
96
96
  #[serde(default)]
97
97
  pub audio_data_uri: Option<String>,
98
98
  #[serde(default)]
99
+ pub video_data_uri: Option<String>,
100
+ #[serde(default)]
99
101
  pub document_attachment: Option<DocumentAttachment>,
100
102
  #[serde(default)]
101
103
  pub workspace_path: Option<String>,
@@ -284,10 +286,12 @@ async fn call_gemini(
284
286
  let url =
285
287
  format!("https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent");
286
288
 
289
+ let payload = gemini_chat_payload(config, request)?;
290
+
287
291
  let response: Value = client
288
292
  .post(url)
289
293
  .header("x-goog-api-key", api_key)
290
- .json(&gemini_chat_payload(config, request)?)
294
+ .json(&payload)
291
295
  .send()
292
296
  .await?
293
297
  .error_for_status()?
@@ -832,20 +836,28 @@ fn require_supported_attachments(provider: &str, request: &ChatRequest) -> Resul
832
836
  .audio_data_uri
833
837
  .as_ref()
834
838
  .is_some_and(|s| !s.trim().is_empty());
839
+ let has_video = request
840
+ .video_data_uri
841
+ .as_ref()
842
+ .is_some_and(|s| !s.trim().is_empty());
835
843
 
836
844
  match provider {
837
845
  "gemini" => Ok(()),
838
- "ollama" if has_audio => Err(ChatError::UnsupportedAttachments(provider.into())),
846
+ "ollama" if has_audio || has_video => {
847
+ Err(ChatError::UnsupportedAttachments(provider.into()))
848
+ }
839
849
  "ollama" => Ok(()),
840
850
  // Custom providers are text-only for now; reject any multimodal content.
841
851
  p if p.starts_with("custom:") => {
842
- if has_image || has_audio {
852
+ if has_image || has_audio || has_video {
843
853
  Err(ChatError::UnsupportedAttachments(provider.into()))
844
854
  } else {
845
855
  Ok(())
846
856
  }
847
857
  }
848
- _ if has_image || has_audio => Err(ChatError::UnsupportedAttachments(provider.into())),
858
+ _ if has_image || has_audio || has_video => {
859
+ Err(ChatError::UnsupportedAttachments(provider.into()))
860
+ }
849
861
  _ => Ok(()),
850
862
  }
851
863
  }
@@ -1035,6 +1047,21 @@ fn gemini_parts(request: &ChatRequest) -> Result<Vec<Value>, ChatError> {
1035
1047
  }));
1036
1048
  }
1037
1049
  }
1050
+ if let Some(ref video_data) = request.video_data_uri {
1051
+ for vid in video_data.split_whitespace() {
1052
+ let payload = vid
1053
+ .strip_prefix("data:")
1054
+ .and_then(|payload| payload.split_once(";base64,"))
1055
+ .filter(|(mime_type, data)| mime_type.starts_with("video/") && !data.is_empty())
1056
+ .ok_or(ChatError::InvalidAttachment)?;
1057
+ parts.push(json!({
1058
+ "inlineData": {
1059
+ "mimeType": payload.0,
1060
+ "data": payload.1
1061
+ }
1062
+ }));
1063
+ }
1064
+ }
1038
1065
  Ok(parts)
1039
1066
  }
1040
1067
 
@@ -1048,6 +1075,26 @@ mod tests {
1048
1075
  assert!(matches!(error, ChatError::MissingApiKey(provider) if provider == "gemini"));
1049
1076
  }
1050
1077
 
1078
+ #[test]
1079
+ fn rejects_video_attachments_for_non_gemini_providers() {
1080
+ let request = ChatRequest {
1081
+ message: "describe this video".into(),
1082
+ system_instruction: String::new(),
1083
+ chat_id: None,
1084
+ image_data_uri: None,
1085
+ audio_data_uri: None,
1086
+ video_data_uri: Some("data:video/mp4;base64,AAAA".into()),
1087
+ document_attachment: None,
1088
+ workspace_path: None,
1089
+ agent_id: None,
1090
+ };
1091
+
1092
+ let error = require_supported_attachments("openai", &request).unwrap_err();
1093
+ assert!(
1094
+ matches!(error, ChatError::UnsupportedAttachments(provider) if provider == "openai")
1095
+ );
1096
+ }
1097
+
1051
1098
  #[test]
1052
1099
  fn parses_stream_provider_formats() {
1053
1100
  assert_eq!(
@@ -1080,6 +1127,7 @@ mod tests {
1080
1127
  chat_id: None,
1081
1128
  image_data_uri: Some("data:image/png;base64,aGk= data:image/jpeg;base64,Ynl5".into()),
1082
1129
  audio_data_uri: None,
1130
+ video_data_uri: None,
1083
1131
  document_attachment: None,
1084
1132
  workspace_path: None,
1085
1133
  agent_id: None,
@@ -6,6 +6,7 @@ use std::{
6
6
 
7
7
  use serde::{Deserialize, Serialize};
8
8
  use sha2::{Digest, Sha256};
9
+ use similar::{ChangeTag, TextDiff};
9
10
  use thiserror::Error;
10
11
 
11
12
  use crate::{Capability, MintConfig, SafetyError, assert_path_capability};
@@ -401,17 +402,34 @@ fn sha256(content: &str) -> String {
401
402
 
402
403
  fn full_file_diff(path: &Path, previous: &str, next: &str) -> String {
403
404
  let label = path.display();
404
- let mut lines = vec![
405
- format!("--- a/{label}"),
406
- format!("+++ b/{label}"),
407
- format!(
408
- "@@ -1,{} +1,{} @@",
409
- previous.lines().count(),
410
- next.lines().count()
411
- ),
412
- ];
413
- lines.extend(previous.lines().map(|line| format!("-{line}")));
414
- lines.extend(next.lines().map(|line| format!("+{line}")));
405
+ let diff = TextDiff::from_lines(previous, next);
406
+ let mut lines = vec![format!("--- a/{label}"), format!("+++ b/{label}")];
407
+
408
+ for group in diff.grouped_ops(3) {
409
+ let mut first = true;
410
+ for op in group {
411
+ if first {
412
+ let old_start = op.old_range().start + 1;
413
+ let old_len = op.old_range().len();
414
+ let new_start = op.new_range().start + 1;
415
+ let new_len = op.new_range().len();
416
+ lines.push(format!(
417
+ "@@ -{old_start},{old_len} +{new_start},{new_len} @@"
418
+ ));
419
+ first = false;
420
+ }
421
+ for change in diff.iter_changes(&op) {
422
+ let sign = match change.tag() {
423
+ ChangeTag::Delete => "-",
424
+ ChangeTag::Insert => "+",
425
+ ChangeTag::Equal => " ",
426
+ };
427
+ let content = change.value().trim_end_matches(&['\r', '\n'][..]);
428
+ lines.push(format!("{sign}{content}"));
429
+ }
430
+ }
431
+ }
432
+
415
433
  lines.join("\n")
416
434
  }
417
435
 
@@ -258,6 +258,54 @@ impl MintConfig {
258
258
  }
259
259
  }
260
260
 
261
+ /// Sets the active provider and model, saves the configuration to disk, and logs a system event interaction.
262
+ pub fn set_active_model(
263
+ &mut self,
264
+ provider: &str,
265
+ model: Option<&str>,
266
+ ) -> Result<String, ConfigError> {
267
+ let old_provider = self.ai_provider.clone();
268
+ let old_active_model = self.active_model().to_string();
269
+
270
+ self.ai_provider = provider.to_string();
271
+ if let Some(m) = model {
272
+ let m_trimmed = m.trim();
273
+ if !m_trimmed.is_empty() {
274
+ match provider {
275
+ "anthropic" => self.anthropic_model = m_trimmed.to_string(),
276
+ "openai" => self.openai_model = m_trimmed.to_string(),
277
+ "openrouter" => self.openrouter_model = m_trimmed.to_string(),
278
+ "deepseek" => self.deepseek_model = m_trimmed.to_string(),
279
+ "huggingface" => self.hf_model = m_trimmed.to_string(),
280
+ "local_openai" => self.local_model_name = m_trimmed.to_string(),
281
+ "ollama" => self.ollama_model = m_trimmed.to_string(),
282
+ "gemini" => self.gemini_model = m_trimmed.to_string(),
283
+ _ => {}
284
+ }
285
+ }
286
+ }
287
+ save_config(self)?;
288
+
289
+ let active_model = self.active_model().to_string();
290
+ let display_name = format!("{} • {}", self.ai_provider, active_model);
291
+
292
+ let changed = old_provider != self.ai_provider || old_active_model != active_model;
293
+ if changed {
294
+ if let Ok(memory) = crate::MemoryStore::open_default() {
295
+ let _ = memory.add_interaction_for_chat_with_fallback(
296
+ crate::CHAT_CLI_ID,
297
+ &display_name,
298
+ "",
299
+ "system",
300
+ "provider_change",
301
+ None,
302
+ );
303
+ }
304
+ }
305
+
306
+ Ok(display_name)
307
+ }
308
+
261
309
  /// Returns the `CustomProvider` whose id matches `provider_key`.
262
310
  ///
263
311
  /// `provider_key` must be in `"custom:<id>"` format (the leading `"custom:"`
@@ -299,6 +347,76 @@ impl MintConfig {
299
347
  }
300
348
  providers
301
349
  }
350
+
351
+ pub fn active_workspace_path(&self) -> Option<PathBuf> {
352
+ self.extra
353
+ .get("activeWorkspacePath")
354
+ .or_else(|| self.extra.get("defaultWorkspacePath"))
355
+ .and_then(Value::as_str)
356
+ .map(str::trim)
357
+ .filter(|s| !s.is_empty())
358
+ .map(PathBuf::from)
359
+ .or_else(|| std::env::current_dir().ok())
360
+ }
361
+
362
+ pub fn line_webhook_host(&self) -> String {
363
+ self.extra
364
+ .get("lineWebhookHost")
365
+ .and_then(Value::as_str)
366
+ .map(str::trim)
367
+ .filter(|s| !s.is_empty())
368
+ .unwrap_or("127.0.0.1")
369
+ .to_string()
370
+ }
371
+
372
+ pub fn line_webhook_port(&self) -> u16 {
373
+ self.extra
374
+ .get("lineWebhookPort")
375
+ .and_then(|v| {
376
+ v.as_u64()
377
+ .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
378
+ })
379
+ .map(|p| p as u16)
380
+ .unwrap_or(3000)
381
+ }
382
+
383
+ pub fn whatsapp_webhook_host(&self) -> String {
384
+ self.extra
385
+ .get("whatsappWebhookHost")
386
+ .and_then(Value::as_str)
387
+ .map(str::trim)
388
+ .filter(|s| !s.is_empty())
389
+ .unwrap_or("127.0.0.1")
390
+ .to_string()
391
+ }
392
+
393
+ pub fn whatsapp_webhook_port(&self) -> u16 {
394
+ self.extra
395
+ .get("whatsappWebhookPort")
396
+ .and_then(|v| {
397
+ v.as_u64()
398
+ .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
399
+ })
400
+ .map(|p| p as u16)
401
+ .unwrap_or(3001)
402
+ }
403
+
404
+ pub fn bridge_ack_enabled(&self) -> bool {
405
+ self.extra
406
+ .get("enableBridgeAckNotification")
407
+ .and_then(Value::as_bool)
408
+ .unwrap_or(true)
409
+ }
410
+
411
+ pub fn bridge_ack_message(&self) -> String {
412
+ self.extra
413
+ .get("bridgeAckMessage")
414
+ .and_then(Value::as_str)
415
+ .map(str::trim)
416
+ .filter(|s| !s.is_empty())
417
+ .unwrap_or("[Mint Agent] Remote command received, processing...")
418
+ .to_string()
419
+ }
302
420
  }
303
421
 
304
422
  pub fn config_path() -> Result<PathBuf, ConfigError> {