@pheem49/mint 1.8.1 → 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 (200) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +0 -17
  4. package/Release_Note.md +137 -58
  5. package/bin/mint +0 -0
  6. package/crates/mint-cli/src/agent.rs +150 -45
  7. package/crates/mint-cli/src/image.rs +8 -8
  8. package/crates/mint-cli/src/main.rs +929 -412
  9. package/crates/mint-cli/src/onboard.rs +108 -108
  10. package/crates/mint-cli/src/setup.rs +143 -143
  11. package/crates/mint-core/src/agent_loop.rs +1 -0
  12. package/crates/mint-core/src/api_server.rs +108 -52
  13. package/crates/mint-core/src/browser.rs +56 -1
  14. package/crates/mint-core/src/channels.rs +1 -0
  15. package/crates/mint-core/src/chat.rs +126 -8
  16. package/crates/mint-core/src/code_tools.rs +18 -20
  17. package/crates/mint-core/src/config.rs +245 -17
  18. package/crates/mint-core/src/image_gen.rs +5 -5
  19. package/crates/mint-core/src/lib.rs +16 -2
  20. package/crates/mint-core/src/memory.rs +6 -1
  21. package/crates/mint-core/src/orchestration.rs +260 -72
  22. package/crates/mint-core/src/pictures.rs +3 -4
  23. package/crates/mint-core/src/semantic.rs +1 -1
  24. package/crates/mint-core/src/shell.rs +1 -1
  25. package/crates/mint-core/src/skills.rs +46 -19
  26. package/crates/mint-core/src/tts.rs +3 -1
  27. package/flatpak/com.pheem49.mint.desktop +9 -0
  28. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  29. package/flatpak/com.pheem49.mint.yaml +52 -0
  30. package/package.json +1 -1
  31. package/shared-modules/.github/workflows/ci.yml +120 -0
  32. package/shared-modules/.github/workflows/update.yaml +28 -0
  33. package/shared-modules/CODEOWNERS +23 -0
  34. package/shared-modules/README.md +79 -0
  35. package/shared-modules/SDL/README.md +3 -0
  36. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  37. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  38. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  39. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  40. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  41. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  42. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  43. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  44. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  45. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  46. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  47. package/shared-modules/SDL/sdl12-compat.json +22 -0
  48. package/shared-modules/SDL2/SDL2_image.json +27 -0
  49. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  50. package/shared-modules/cld2/CMakeLists.txt +157 -0
  51. package/shared-modules/cld2/cld2.json +24 -0
  52. package/shared-modules/clutter/clutter.json +72 -0
  53. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  54. package/shared-modules/glew/glew.json +36 -0
  55. package/shared-modules/glu/glu-9.json +12 -0
  56. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  57. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  58. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  59. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  60. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  61. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  62. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  63. package/shared-modules/gtk2/gtk2.json +120 -0
  64. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  65. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  66. package/shared-modules/gtk2/readme.md +10 -0
  67. package/shared-modules/intltool/intltool-0.51.json +20 -0
  68. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  69. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  70. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  71. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  72. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  73. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  74. package/shared-modules/lame/lame-3.99.5.json +32 -0
  75. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  76. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  77. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  78. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  79. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  80. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  81. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  82. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  83. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  84. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  85. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  86. package/shared-modules/libcanberra/libcanberra.json +44 -0
  87. package/shared-modules/libmad/libmad.json +19 -0
  88. package/shared-modules/libsecret/libsecret.json +23 -0
  89. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  90. package/shared-modules/libusb/libusb.json +27 -0
  91. package/shared-modules/linux-audio/dssi.json +31 -0
  92. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  93. package/shared-modules/linux-audio/fftw3f.json +42 -0
  94. package/shared-modules/linux-audio/fluidsynth2-static.json +36 -0
  95. package/shared-modules/linux-audio/fluidsynth2.json +25 -0
  96. package/shared-modules/linux-audio/ladspa.json +26 -0
  97. package/shared-modules/linux-audio/lash.json +35 -0
  98. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  99. package/shared-modules/linux-audio/liblo-static.json +22 -0
  100. package/shared-modules/linux-audio/liblo.json +17 -0
  101. package/shared-modules/linux-audio/lilv-static.json +94 -0
  102. package/shared-modules/linux-audio/lilv.json +89 -0
  103. package/shared-modules/linux-audio/lrdf.json +57 -0
  104. package/shared-modules/linux-audio/lv2.json +20 -0
  105. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  106. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  107. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  108. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  109. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  110. package/shared-modules/linux-audio/stk.json +28 -0
  111. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  112. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  113. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  114. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  115. package/shared-modules/lua5.3/lua.pc.in +8 -0
  116. package/shared-modules/lua5.3/luaconf.patch +47 -0
  117. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  118. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  119. package/shared-modules/lua5.4/lua.pc.in +8 -0
  120. package/shared-modules/lua5.4/luaconf.patch +34 -0
  121. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  122. package/shared-modules/luajit/luajit.json +30 -0
  123. package/shared-modules/lzo/lzo.json +27 -0
  124. package/shared-modules/mac/mac.json +18 -0
  125. package/shared-modules/physfs/physfs.json +26 -0
  126. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  127. package/shared-modules/pygame/cython-fixes.patch +34 -0
  128. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  129. package/shared-modules/pygame/glib.patch +25 -0
  130. package/shared-modules/pygame/pygame1-python2.json +157 -0
  131. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  132. package/shared-modules/pygtk/pygtk.json +117 -0
  133. package/shared-modules/pygtk/pygtk.patch +39 -0
  134. package/shared-modules/python2.7/python-2.7.json +48 -0
  135. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  136. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  137. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  138. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  139. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  140. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  141. package/shared-modules/update-checker.sh +50 -0
  142. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  143. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  144. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  145. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  146. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  147. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  148. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  149. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  150. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  151. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  152. package/shared-modules/vorbisgain/README.md +23 -0
  153. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  154. package/src/renderer/shared/agentProgress.ts +145 -0
  155. package/src/renderer/shared/calculator.ts +52 -0
  156. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  157. package/src/renderer/shared/components/AgentActivityTable.tsx +85 -0
  158. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  159. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  160. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  161. package/src/renderer/shared/constants/models.ts +91 -0
  162. package/src/renderer/shared/platform.ts +82 -0
  163. package/src/renderer/shared/types.ts +140 -0
  164. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  165. package/src/renderer/shared/utils/approval.ts +1 -0
  166. package/src/renderer/shared/utils/approval.tsx +64 -0
  167. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  168. package/src/renderer/shared/utils/markdown.tsx +347 -0
  169. package/src/renderer/shared/utils/providers.ts +36 -0
  170. package/src/renderer/shared/utils/speech.ts +216 -0
  171. package/src/renderer/shared/utils/ui.ts +147 -0
  172. package/src/renderer/src/agentProgress.ts +11 -39
  173. package/src/renderer/src/calculator.ts +3 -46
  174. package/src/renderer/src/components/ChatPanel.tsx +219 -1308
  175. package/src/renderer/src/components/MintDashboard.tsx +67 -170
  176. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  177. package/src/renderer/src/components/Settings/AgentsTab.tsx +381 -0
  178. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  179. package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
  180. package/src/renderer/src/components/SettingsWindow.tsx +120 -3
  181. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  182. package/src/renderer/src/css/styles.css +100 -0
  183. package/src/renderer/src/tauri.ts +128 -143
  184. package/src/renderer/src-web/agentProgress.ts +11 -39
  185. package/src/renderer/src-web/calculator.ts +3 -46
  186. package/src/renderer/src-web/components/ChatPanel.tsx +219 -1306
  187. package/src/renderer/src-web/components/MintDashboard.tsx +42 -170
  188. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  189. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +381 -0
  190. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  191. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
  192. package/src/renderer/src-web/components/SettingsWindow.tsx +127 -3
  193. package/src/renderer/src-web/css/styles.css +100 -0
  194. package/src/renderer/src-web/tauri.ts +180 -102
  195. package/src-tauri/src/desktop.rs +18 -16
  196. package/src-tauri/src/discord_rpc.rs +1 -1
  197. package/src-tauri/src/lib.rs +168 -40
  198. package/src-tauri/src/webhooks.rs +1 -0
  199. package/src-tauri/src/workflows.rs +1 -0
  200. package/src-tauri/tauri.conf.json +1 -1
@@ -79,10 +79,65 @@ pub struct MintConfig {
79
79
  pub blocked_paths: Vec<PathBuf>,
80
80
  pub blocked_file_names: Vec<String>,
81
81
  pub disabled_tools: Vec<String>,
82
+ pub agents: Vec<AgentConfig>,
83
+ pub enable_agent_collaboration: bool,
84
+ /// User-defined OpenAI-compatible providers.
85
+ pub custom_providers: Vec<CustomProvider>,
82
86
  #[serde(flatten)]
83
87
  pub extra: BTreeMap<String, Value>,
84
88
  }
85
89
 
90
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
91
+ #[serde(rename_all = "camelCase")]
92
+ pub struct AgentConfig {
93
+ pub id: String,
94
+ pub name: String,
95
+ pub provider: String,
96
+ pub model: String,
97
+ pub api_key: Option<String>,
98
+ pub system_instruction: String,
99
+ pub enabled: bool,
100
+ }
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
+
86
141
  impl Default for MintConfig {
87
142
  fn default() -> Self {
88
143
  let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
@@ -142,36 +197,106 @@ impl Default for MintConfig {
142
197
  ],
143
198
  blocked_file_names: vec![".env".into(), "id_rsa".into(), "id_ed25519".into()],
144
199
  disabled_tools: Vec::new(),
200
+ agents: default_agents(),
201
+ enable_agent_collaboration: false,
202
+ custom_providers: Vec::new(),
145
203
  extra: runtime_extra_defaults(),
146
204
  }
147
205
  }
148
206
  }
149
207
 
208
+ fn default_agents() -> Vec<AgentConfig> {
209
+ vec![
210
+ AgentConfig {
211
+ id: "planner".into(),
212
+ name: "Planner".into(),
213
+ provider: "gemini".into(),
214
+ model: "gemini-2.5-flash".into(),
215
+ api_key: None,
216
+ system_instruction: "You are the Planner agent. Your task is to analyze the user request, inspect the workspace structure, and design a step-by-step implementation plan. Create or update the implementation_plan.md file to document your proposal.".into(),
217
+ enabled: true,
218
+ },
219
+ AgentConfig {
220
+ id: "coder".into(),
221
+ name: "Coder".into(),
222
+ provider: "gemini".into(),
223
+ model: "gemini-2.5-flash".into(),
224
+ api_key: None,
225
+ system_instruction: "You are the Coder agent. Your task is to implement the changes specified in the approved implementation plan. Read relevant files, write clean and efficient code, and run terminal commands to build or verify. Stay focused on execution.".into(),
226
+ enabled: true,
227
+ },
228
+ AgentConfig {
229
+ id: "reviewer".into(),
230
+ name: "Reviewer".into(),
231
+ provider: "gemini".into(),
232
+ model: "gemini-2.5-flash".into(),
233
+ api_key: None,
234
+ system_instruction: "You are the Reviewer agent. Your task is to verify the code modifications made by the Coder. Run the automated tests, check lint errors, and ensure the implementation is correct and complete.".into(),
235
+ enabled: true,
236
+ },
237
+ ]
238
+ }
239
+
150
240
  impl MintConfig {
151
- pub fn available_providers(&self) -> Vec<&'static str> {
241
+ pub fn active_model(&self) -> &str {
242
+ match self.ai_provider.as_str() {
243
+ "anthropic" => &self.anthropic_model,
244
+ "openai" => &self.openai_model,
245
+ "openrouter" => &self.openrouter_model,
246
+ "deepseek" => &self.deepseek_model,
247
+ "huggingface" => &self.hf_model,
248
+ "local_openai" => &self.local_model_name,
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
+ }
257
+ _ => &self.gemini_model,
258
+ }
259
+ }
260
+
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> {
152
271
  let mut providers = Vec::new();
153
272
  if has_value(&self.anthropic_api_key) {
154
- providers.push("anthropic");
273
+ providers.push("anthropic".to_owned());
155
274
  }
156
275
  if has_value(&self.openai_api_key) {
157
- providers.push("openai");
276
+ providers.push("openai".to_owned());
158
277
  }
159
278
  if has_value(&self.openrouter_api_key) {
160
- providers.push("openrouter");
279
+ providers.push("openrouter".to_owned());
161
280
  }
162
281
  if has_value(&self.deepseek_api_key) {
163
- providers.push("deepseek");
282
+ providers.push("deepseek".to_owned());
164
283
  }
165
284
  if has_value(&self.api_key) {
166
- providers.push("gemini");
285
+ providers.push("gemini".to_owned());
167
286
  }
168
287
  if has_value(&self.hf_api_key) {
169
- providers.push("huggingface");
288
+ providers.push("huggingface".to_owned());
170
289
  }
171
290
  if has_value(&self.local_api_base_url) {
172
- 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
+ }
173
299
  }
174
- providers.push("ollama");
175
300
  providers
176
301
  }
177
302
  }
@@ -377,7 +502,10 @@ mod tests {
377
502
 
378
503
  #[test]
379
504
  fn default_config_always_exposes_ollama() {
380
- assert_eq!(MintConfig::default().available_providers(), vec!["ollama"]);
505
+ assert_eq!(
506
+ MintConfig::default().available_providers(),
507
+ vec!["ollama".to_owned()]
508
+ );
381
509
  }
382
510
 
383
511
  #[test]
@@ -393,12 +521,12 @@ mod tests {
393
521
  assert_eq!(
394
522
  config.available_providers(),
395
523
  vec![
396
- "openai",
397
- "openrouter",
398
- "deepseek",
399
- "gemini",
400
- "local_openai",
401
- "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(),
402
530
  ]
403
531
  );
404
532
  }
@@ -448,8 +576,108 @@ mod tests {
448
576
  save_config_to(&path, &initial_config).unwrap();
449
577
 
450
578
  let loaded = load_config_from(&path).unwrap();
451
- assert!(loaded.theme.len() > 0);
579
+ assert!(!loaded.theme.is_empty());
452
580
 
453
581
  let _ = std::fs::remove_file(&path);
454
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
+ }
455
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::{
@@ -91,3 +91,17 @@ pub use channels::start_channels;
91
91
 
92
92
  pub static HTTP_CLIENT: std::sync::LazyLock<reqwest::Client> =
93
93
  std::sync::LazyLock::new(reqwest::Client::new);
94
+
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()));
98
+
99
+ pub fn cancel_agent(chat_id: &str) -> bool {
100
+ let mut agents = ACTIVE_AGENTS.lock().unwrap();
101
+ if let Some(handle) = agents.remove(chat_id) {
102
+ handle.abort();
103
+ true
104
+ } else {
105
+ false
106
+ }
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