@pheem49/mint 1.8.2 → 1.9.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 (44) hide show
  1. package/Cargo.toml +1 -1
  2. package/Release_Note.md +48 -3
  3. package/crates/mint-cli/src/agent.rs +95 -84
  4. package/crates/mint-cli/src/image.rs +8 -8
  5. package/crates/mint-cli/src/main.rs +806 -403
  6. package/crates/mint-cli/src/onboard.rs +108 -108
  7. package/crates/mint-cli/src/setup.rs +143 -143
  8. package/crates/mint-core/src/api_server.rs +57 -54
  9. package/crates/mint-core/src/browser.rs +1 -1
  10. package/crates/mint-core/src/chat.rs +123 -8
  11. package/crates/mint-core/src/code_tools.rs +18 -20
  12. package/crates/mint-core/src/config.rs +184 -17
  13. package/crates/mint-core/src/image_gen.rs +5 -5
  14. package/crates/mint-core/src/lib.rs +5 -5
  15. package/crates/mint-core/src/memory.rs +6 -1
  16. package/crates/mint-core/src/orchestration.rs +104 -85
  17. package/crates/mint-core/src/pictures.rs +3 -4
  18. package/crates/mint-core/src/semantic.rs +1 -1
  19. package/crates/mint-core/src/shell.rs +1 -1
  20. package/crates/mint-core/src/skills.rs +46 -19
  21. package/package.json +1 -1
  22. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  23. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  24. package/shared-modules/linux-audio/lilv.json +26 -19
  25. package/shared-modules/linux-audio/lv2.json +0 -1
  26. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  27. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  28. package/src/renderer/src/components/ChatPanel.tsx +25 -5
  29. package/src/renderer/src/components/MintDashboard.tsx +45 -2
  30. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  31. package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
  32. package/src/renderer/src/components/SettingsWindow.tsx +68 -1
  33. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  34. package/src/renderer/src/css/styles.css +16 -0
  35. package/src/renderer/src-web/components/ChatPanel.tsx +25 -5
  36. package/src/renderer/src-web/components/MintDashboard.tsx +19 -1
  37. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  38. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
  39. package/src/renderer/src-web/components/SettingsWindow.tsx +75 -1
  40. package/src/renderer/src-web/css/styles.css +16 -0
  41. package/src-tauri/src/desktop.rs +18 -16
  42. package/src-tauri/src/discord_rpc.rs +1 -1
  43. package/src-tauri/src/lib.rs +41 -17
  44. package/src-tauri/tauri.conf.json +1 -1
@@ -81,6 +81,8 @@ pub struct MintConfig {
81
81
  pub disabled_tools: Vec<String>,
82
82
  pub agents: Vec<AgentConfig>,
83
83
  pub enable_agent_collaboration: bool,
84
+ /// User-defined OpenAI-compatible providers.
85
+ pub custom_providers: Vec<CustomProvider>,
84
86
  #[serde(flatten)]
85
87
  pub extra: BTreeMap<String, Value>,
86
88
  }
@@ -97,6 +99,45 @@ pub struct AgentConfig {
97
99
  pub enabled: bool,
98
100
  }
99
101
 
102
+ /// A single model entry inside a [`CustomProvider`].
103
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
104
+ #[serde(rename_all = "camelCase")]
105
+ pub struct CustomProviderModel {
106
+ /// The model identifier sent to the API, e.g. `"llama-3.1-8b"`.
107
+ pub model_id: String,
108
+ /// Human-readable label shown in the UI, e.g. `"Llama 3.1 8B"`.
109
+ pub display_name: String,
110
+ }
111
+
112
+ /// An extra HTTP header to include with every request to a [`CustomProvider`].
113
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
114
+ #[serde(rename_all = "camelCase")]
115
+ pub struct CustomProviderHeader {
116
+ pub name: String,
117
+ pub value: String,
118
+ }
119
+
120
+ /// A fully user-defined, OpenAI-compatible AI provider.
121
+ ///
122
+ /// When this provider is active the `ai_provider` config field is set to
123
+ /// `"custom:<id>"` where `<id>` is [`CustomProvider::id`].
124
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
125
+ #[serde(rename_all = "camelCase")]
126
+ pub struct CustomProvider {
127
+ /// Slug identifier (lowercase letters, numbers, hyphens, underscores).
128
+ pub id: String,
129
+ /// Human-readable name shown in the UI.
130
+ pub display_name: String,
131
+ /// Base URL of the OpenAI-compatible endpoint, e.g. `"http://localhost:20128/v1"`.
132
+ pub base_url: String,
133
+ /// API key sent as `Authorization: Bearer <key>`. Empty = no auth header.
134
+ pub api_key: String,
135
+ /// Models offered by this provider.
136
+ pub models: Vec<CustomProviderModel>,
137
+ /// Optional extra HTTP headers sent with every request.
138
+ pub headers: Vec<CustomProviderHeader>,
139
+ }
140
+
100
141
  impl Default for MintConfig {
101
142
  fn default() -> Self {
102
143
  let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
@@ -158,6 +199,7 @@ impl Default for MintConfig {
158
199
  disabled_tools: Vec::new(),
159
200
  agents: default_agents(),
160
201
  enable_agent_collaboration: false,
202
+ custom_providers: Vec::new(),
161
203
  extra: runtime_extra_defaults(),
162
204
  }
163
205
  }
@@ -205,34 +247,56 @@ impl MintConfig {
205
247
  "huggingface" => &self.hf_model,
206
248
  "local_openai" => &self.local_model_name,
207
249
  "ollama" => &self.ollama_model,
250
+ p if p.starts_with("custom:") => {
251
+ // Return the first model id of the matching custom provider, if any.
252
+ self.resolve_custom_provider(p)
253
+ .and_then(|cp| cp.models.first())
254
+ .map(|m| m.model_id.as_str())
255
+ .unwrap_or("")
256
+ }
208
257
  _ => &self.gemini_model,
209
258
  }
210
259
  }
211
260
 
212
- pub fn available_providers(&self) -> Vec<&'static str> {
261
+ /// Returns the `CustomProvider` whose id matches `provider_key`.
262
+ ///
263
+ /// `provider_key` must be in `"custom:<id>"` format (the leading `"custom:"`
264
+ /// prefix is stripped before the lookup).
265
+ pub fn resolve_custom_provider(&self, provider_key: &str) -> Option<&CustomProvider> {
266
+ let id = provider_key.strip_prefix("custom:").unwrap_or(provider_key);
267
+ self.custom_providers.iter().find(|cp| cp.id == id)
268
+ }
269
+
270
+ pub fn available_providers(&self) -> Vec<String> {
213
271
  let mut providers = Vec::new();
214
272
  if has_value(&self.anthropic_api_key) {
215
- providers.push("anthropic");
273
+ providers.push("anthropic".to_owned());
216
274
  }
217
275
  if has_value(&self.openai_api_key) {
218
- providers.push("openai");
276
+ providers.push("openai".to_owned());
219
277
  }
220
278
  if has_value(&self.openrouter_api_key) {
221
- providers.push("openrouter");
279
+ providers.push("openrouter".to_owned());
222
280
  }
223
281
  if has_value(&self.deepseek_api_key) {
224
- providers.push("deepseek");
282
+ providers.push("deepseek".to_owned());
225
283
  }
226
284
  if has_value(&self.api_key) {
227
- providers.push("gemini");
285
+ providers.push("gemini".to_owned());
228
286
  }
229
287
  if has_value(&self.hf_api_key) {
230
- providers.push("huggingface");
288
+ providers.push("huggingface".to_owned());
231
289
  }
232
290
  if has_value(&self.local_api_base_url) {
233
- providers.push("local_openai");
291
+ providers.push("local_openai".to_owned());
292
+ }
293
+ providers.push("ollama".to_owned());
294
+ // Include all user-defined custom providers that have a base URL configured.
295
+ for cp in &self.custom_providers {
296
+ if !cp.base_url.trim().is_empty() {
297
+ providers.push(format!("custom:{}", cp.id));
298
+ }
234
299
  }
235
- providers.push("ollama");
236
300
  providers
237
301
  }
238
302
  }
@@ -438,7 +502,10 @@ mod tests {
438
502
 
439
503
  #[test]
440
504
  fn default_config_always_exposes_ollama() {
441
- assert_eq!(MintConfig::default().available_providers(), vec!["ollama"]);
505
+ assert_eq!(
506
+ MintConfig::default().available_providers(),
507
+ vec!["ollama".to_owned()]
508
+ );
442
509
  }
443
510
 
444
511
  #[test]
@@ -454,12 +521,12 @@ mod tests {
454
521
  assert_eq!(
455
522
  config.available_providers(),
456
523
  vec![
457
- "openai",
458
- "openrouter",
459
- "deepseek",
460
- "gemini",
461
- "local_openai",
462
- "ollama"
524
+ "openai".to_owned(),
525
+ "openrouter".to_owned(),
526
+ "deepseek".to_owned(),
527
+ "gemini".to_owned(),
528
+ "local_openai".to_owned(),
529
+ "ollama".to_owned(),
463
530
  ]
464
531
  );
465
532
  }
@@ -509,8 +576,108 @@ mod tests {
509
576
  save_config_to(&path, &initial_config).unwrap();
510
577
 
511
578
  let loaded = load_config_from(&path).unwrap();
512
- assert!(loaded.theme.len() > 0);
579
+ assert!(!loaded.theme.is_empty());
513
580
 
514
581
  let _ = std::fs::remove_file(&path);
515
582
  }
583
+
584
+ #[test]
585
+ fn custom_provider_round_trips() {
586
+ let config = MintConfig {
587
+ custom_providers: vec![CustomProvider {
588
+ id: "myprovider".into(),
589
+ display_name: "My Provider".into(),
590
+ base_url: "http://localhost:20128/v1".into(),
591
+ api_key: "sk-abc".into(),
592
+ models: vec![CustomProviderModel {
593
+ model_id: "llama-3.1-8b".into(),
594
+ display_name: "Llama 3.1 8B".into(),
595
+ }],
596
+ headers: vec![CustomProviderHeader {
597
+ name: "X-Custom".into(),
598
+ value: "test".into(),
599
+ }],
600
+ }],
601
+ ..MintConfig::default()
602
+ };
603
+ let json = serde_json::to_string(&config).unwrap();
604
+ let restored: MintConfig = serde_json::from_str(&json).unwrap();
605
+ assert_eq!(restored, config);
606
+ }
607
+
608
+ #[test]
609
+ fn custom_provider_appears_in_available_providers() {
610
+ let config = MintConfig {
611
+ custom_providers: vec![CustomProvider {
612
+ id: "myprovider".into(),
613
+ base_url: "http://localhost:20128/v1".into(),
614
+ ..CustomProvider::default()
615
+ }],
616
+ ..MintConfig::default()
617
+ };
618
+ assert!(
619
+ config
620
+ .available_providers()
621
+ .contains(&"custom:myprovider".to_owned())
622
+ );
623
+ }
624
+
625
+ #[test]
626
+ fn custom_provider_without_base_url_excluded() {
627
+ let config = MintConfig {
628
+ custom_providers: vec![CustomProvider {
629
+ id: "empty".into(),
630
+ base_url: "".into(),
631
+ ..CustomProvider::default()
632
+ }],
633
+ ..MintConfig::default()
634
+ };
635
+ assert!(
636
+ !config
637
+ .available_providers()
638
+ .contains(&"custom:empty".to_owned())
639
+ );
640
+ }
641
+
642
+ #[test]
643
+ fn active_model_resolves_first_custom_model() {
644
+ let config = MintConfig {
645
+ ai_provider: "custom:myprovider".into(),
646
+ custom_providers: vec![CustomProvider {
647
+ id: "myprovider".into(),
648
+ base_url: "http://localhost:20128/v1".into(),
649
+ models: vec![CustomProviderModel {
650
+ model_id: "llama-3.1-8b".into(),
651
+ display_name: String::new(),
652
+ }],
653
+ ..CustomProvider::default()
654
+ }],
655
+ ..MintConfig::default()
656
+ };
657
+ assert_eq!(config.active_model(), "llama-3.1-8b");
658
+ }
659
+
660
+ #[test]
661
+ fn resolve_custom_provider_returns_none_for_unknown() {
662
+ let config = MintConfig::default();
663
+ assert!(
664
+ config
665
+ .resolve_custom_provider("custom:nonexistent")
666
+ .is_none()
667
+ );
668
+ }
669
+
670
+ #[test]
671
+ fn resolve_custom_provider_strips_prefix() {
672
+ let config = MintConfig {
673
+ custom_providers: vec![CustomProvider {
674
+ id: "myp".into(),
675
+ ..CustomProvider::default()
676
+ }],
677
+ ..MintConfig::default()
678
+ };
679
+ assert!(config.resolve_custom_provider("custom:myp").is_some());
680
+ // Also works without prefix
681
+ assert!(config.resolve_custom_provider("myp").is_some());
682
+ }
516
683
  }
@@ -659,11 +659,11 @@ async fn call_replicate(
659
659
  poll_replicate(client, &api_key, &prediction_url).await?
660
660
  };
661
661
 
662
- if let Some(err) = prediction.get("error") {
663
- if !err.is_null() {
664
- let msg = err.as_str().unwrap_or("unknown Replicate error");
665
- return Err(ImageGenError::ModelError(msg.to_owned()));
666
- }
662
+ if let Some(err) = prediction.get("error")
663
+ && !err.is_null()
664
+ {
665
+ let msg = err.as_str().unwrap_or("unknown Replicate error");
666
+ return Err(ImageGenError::ModelError(msg.to_owned()));
667
667
  }
668
668
 
669
669
  // Output is an array of image URLs
@@ -41,8 +41,8 @@ pub use code_tools::{
41
41
  propose_code_edits, read_code_file, repository_summary, search_code,
42
42
  };
43
43
  pub use config::{
44
- ConfigError, MintConfig, config_path, initialize_config, load_config, save_config,
45
- set_config_value,
44
+ AgentConfig, ConfigError, CustomProvider, CustomProviderHeader, CustomProviderModel,
45
+ MintConfig, config_path, initialize_config, load_config, save_config, set_config_value,
46
46
  };
47
47
  pub use files::{FileOperationError, PathKind, PathMatch, create_folder, find_paths};
48
48
  pub use image_gen::{
@@ -92,8 +92,9 @@ pub use channels::start_channels;
92
92
  pub static HTTP_CLIENT: std::sync::LazyLock<reqwest::Client> =
93
93
  std::sync::LazyLock::new(reqwest::Client::new);
94
94
 
95
- pub static ACTIVE_AGENTS: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<String, tokio::task::AbortHandle>>> =
96
- std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
95
+ pub static ACTIVE_AGENTS: std::sync::LazyLock<
96
+ std::sync::Mutex<std::collections::HashMap<String, tokio::task::AbortHandle>>,
97
+ > = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
97
98
 
98
99
  pub fn cancel_agent(chat_id: &str) -> bool {
99
100
  let mut agents = ACTIVE_AGENTS.lock().unwrap();
@@ -104,4 +105,3 @@ pub fn cancel_agent(chat_id: &str) -> bool {
104
105
  false
105
106
  }
106
107
  }
107
-
@@ -58,6 +58,8 @@ pub struct LearnedSkill {
58
58
  pub source_path: String,
59
59
  pub content: String,
60
60
  pub created_at: String,
61
+ #[serde(default)]
62
+ pub description: Option<String>,
61
63
  }
62
64
 
63
65
  #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
@@ -624,12 +626,15 @@ fn ensure_column(
624
626
  }
625
627
 
626
628
  fn learned_skill_row(row: &rusqlite::Row<'_>) -> Result<LearnedSkill, rusqlite::Error> {
629
+ let content: String = row.get(3)?;
630
+ let description = crate::skills::parse_skill_description(&content);
627
631
  Ok(LearnedSkill {
628
632
  id: row.get(0)?,
629
633
  name: row.get(1)?,
630
634
  source_path: row.get(2)?,
631
- content: row.get(3)?,
635
+ content,
632
636
  created_at: row.get(4)?,
637
+ description,
633
638
  })
634
639
  }
635
640