@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
@@ -7,7 +7,8 @@ import {
7
7
  DEEPSEEK_MODELS,
8
8
  ANTHROPIC_MODELS,
9
9
  HF_MODELS,
10
- LOCAL_MODELS
10
+ LOCAL_MODELS,
11
+ CustomProviderConfig,
11
12
  } from '../SettingsWindow'
12
13
 
13
14
  interface Agent {
@@ -21,7 +22,7 @@ interface Agent {
21
22
  }
22
23
 
23
24
  interface AgentsTabProps {
24
- config: typeof DEFAULT_CONFIG & { agents?: Agent[] }
25
+ config: typeof DEFAULT_CONFIG & { agents?: Agent[], customProviders?: CustomProviderConfig[] }
25
26
  updateField: (field: any, value: any) => void
26
27
  dynamicOllamaModels?: string[]
27
28
  }
@@ -47,6 +48,11 @@ export default function AgentsTab({ config, updateField, dynamicOllamaModels = [
47
48
  else if (newProvider === 'huggingface') defaultModel = config.hfModel || HF_MODELS[0]
48
49
  else if (newProvider === 'local_openai') defaultModel = config.localModelName || LOCAL_MODELS[0]
49
50
  else if (newProvider === 'ollama') defaultModel = config.ollamaModel || dynamicOllamaModels[0] || 'llama3:latest'
51
+ else if (newProvider.startsWith('custom:')) {
52
+ const cpId = newProvider.replace(/^custom:/, '')
53
+ const cp = (config.customProviders ?? []).find(p => p.id === cpId)
54
+ defaultModel = cp?.models[0]?.modelId ?? ''
55
+ }
50
56
  setModel(defaultModel)
51
57
  }
52
58
 
@@ -141,6 +147,14 @@ export default function AgentsTab({ config, updateField, dynamicOllamaModels = [
141
147
  { value: 'ollama', label: 'Ollama (Local)', hasKey: true },
142
148
  { value: 'huggingface', label: 'Hugging Face', hasKey: !!config.hfApiKey },
143
149
  { value: 'local_openai', label: 'Local (LM Studio / OpenAI Compatible)', hasKey: true },
150
+ // Custom providers — available whenever they have a base URL configured
151
+ ...(config.customProviders ?? [])
152
+ .filter(cp => cp.baseUrl.trim().length > 0)
153
+ .map(cp => ({
154
+ value: `custom:${cp.id}`,
155
+ label: `${cp.displayName || cp.id} (Custom)`,
156
+ hasKey: true,
157
+ })),
144
158
  ].filter(p => p.hasKey);
145
159
 
146
160
  // Find out what models list corresponds to the selected provider
@@ -170,6 +184,11 @@ export default function AgentsTab({ config, updateField, dynamicOllamaModels = [
170
184
  } else if (provider === 'ollama') {
171
185
  modelList = dynamicOllamaModels
172
186
  defaultGeneralModel = config.ollamaModel
187
+ } else if (provider.startsWith('custom:')) {
188
+ const cpId = provider.replace(/^custom:/, '')
189
+ const cp = (config.customProviders ?? []).find(p => p.id === cpId)
190
+ modelList = cp?.models.map(m => m.modelId) ?? []
191
+ defaultGeneralModel = modelList[0] ?? ''
173
192
  }
174
193
 
175
194
  // Sort modelList so defaultGeneralModel is first (at the top)
@@ -7,7 +7,10 @@ import {
7
7
  DEEPSEEK_MODELS,
8
8
  ANTHROPIC_MODELS,
9
9
  HF_MODELS,
10
- LOCAL_MODELS
10
+ LOCAL_MODELS,
11
+ CustomProviderConfig,
12
+ CustomProviderModel,
13
+ CustomProviderHeader,
11
14
  } from '../SettingsWindow'
12
15
 
13
16
  interface GeneralTabProps {
@@ -35,6 +38,7 @@ interface GeneralTabProps {
35
38
  handleCheckUpdates: () => void
36
39
  handleInstallUpdate: () => void
37
40
  isDesktopApp?: boolean
41
+ onSaveWithoutClosing?: () => Promise<void>
38
42
  }
39
43
 
40
44
  export default function GeneralTab({
@@ -61,8 +65,18 @@ export default function GeneralTab({
61
65
  updateMessage,
62
66
  handleCheckUpdates,
63
67
  handleInstallUpdate,
64
- isDesktopApp = false
68
+ isDesktopApp = false,
69
+ onSaveWithoutClosing
65
70
  }: GeneralTabProps) {
71
+ const [savedProviderId, setSavedProviderId] = React.useState<string | null>(null)
72
+
73
+ const handleSaveProvider = async (providerId: string) => {
74
+ if (onSaveWithoutClosing) {
75
+ await onSaveWithoutClosing()
76
+ setSavedProviderId(providerId)
77
+ setTimeout(() => setSavedProviderId(null), 2000)
78
+ }
79
+ }
66
80
  return (
67
81
  <div className="tab-pane active">
68
82
  <section className="setting-section">
@@ -86,8 +100,38 @@ export default function GeneralTab({
86
100
  <option value="ollama">Ollama (Local / Private)</option>
87
101
  <option value="huggingface">Hugging Face (Inference API)</option>
88
102
  <option value="local_openai">Local (LM Studio / OpenAI Compatible)</option>
103
+ {(config.customProviders ?? []).map(cp => (
104
+ <option key={`custom:${cp.id}`} value={`custom:${cp.id}`}>
105
+ {cp.displayName || cp.id} (Custom)
106
+ </option>
107
+ ))}
89
108
  </select>
90
109
  </div>
110
+ {/* Model selector for custom providers */}
111
+ {config.aiProvider.startsWith('custom:') && (() => {
112
+ const activeId = config.aiProvider.replace(/^custom:/, '')
113
+ const cp = (config.customProviders ?? []).find(p => p.id === activeId)
114
+ if (!cp || cp.models.length === 0) return null
115
+ const currentModel = (config.customModelSelections ?? {})[activeId] ?? cp.models[0]?.modelId ?? ''
116
+ return (
117
+ <div className="setting-row wide">
118
+ <label>Active Model</label>
119
+ <select
120
+ value={currentModel}
121
+ onChange={(e) => updateField('customModelSelections', {
122
+ ...(config.customModelSelections ?? {}),
123
+ [activeId]: e.target.value
124
+ })}
125
+ >
126
+ {cp.models.map(m => (
127
+ <option key={m.modelId} value={m.modelId}>
128
+ {m.displayName || m.modelId}
129
+ </option>
130
+ ))}
131
+ </select>
132
+ </div>
133
+ )
134
+ })()}
91
135
  </div>
92
136
 
93
137
  <div className="provider-cards-container">
@@ -877,12 +921,219 @@ export default function GeneralTab({
877
921
  </div>
878
922
  </section>
879
923
 
924
+ {/* ─── Custom Providers ─────────────────────────────────────────────────── */}
925
+ <section className="setting-section">
926
+ <div className="section-heading">
927
+ <div>
928
+ <p className="section-kicker">Custom AI Routing</p>
929
+ <h2 className="section-title">Custom Providers</h2>
930
+ </div>
931
+ <p className="section-description">
932
+ Add any OpenAI-compatible endpoint as a custom provider. Configure its models and optional headers.
933
+ </p>
934
+ </div>
935
+
936
+ <div className="provider-cards-container">
937
+ {(config.customProviders ?? []).map((cp, cpIdx) => {
938
+ const isActive = config.aiProvider === `custom:${cp.id}`
939
+
940
+ const updateCp = (patch: Partial<CustomProviderConfig>) => {
941
+ const updated = (config.customProviders ?? []).map((p, i) =>
942
+ i === cpIdx ? { ...p, ...patch } : p
943
+ )
944
+ updateField('customProviders', updated)
945
+ }
946
+
947
+ const deleteCp = () => {
948
+ const updated = (config.customProviders ?? []).filter((_, i) => i !== cpIdx)
949
+ updateField('customProviders', updated)
950
+ if (config.aiProvider === `custom:${cp.id}`) {
951
+ updateField('aiProvider', 'gemini')
952
+ }
953
+ }
954
+
955
+ const updateModel = (mIdx: number, patch: Partial<CustomProviderModel>) => {
956
+ const updatedModels = cp.models.map((m, i) => i === mIdx ? { ...m, ...patch } : m)
957
+ updateCp({ models: updatedModels })
958
+ }
959
+ const deleteModel = (mIdx: number) => {
960
+ updateCp({ models: cp.models.filter((_, i) => i !== mIdx) })
961
+ }
962
+ const addModel = () => {
963
+ updateCp({ models: [...cp.models, { modelId: '', displayName: '' }] })
964
+ }
965
+
966
+ const updateHeader = (hIdx: number, patch: Partial<CustomProviderHeader>) => {
967
+ const updatedHeaders = cp.headers.map((h, i) => i === hIdx ? { ...h, ...patch } : h)
968
+ updateCp({ headers: updatedHeaders })
969
+ }
970
+ const deleteHeader = (hIdx: number) => {
971
+ updateCp({ headers: cp.headers.filter((_, i) => i !== hIdx) })
972
+ }
973
+ const addHeader = () => {
974
+ updateCp({ headers: [...cp.headers, { name: '', value: '' }] })
975
+ }
976
+
977
+ return (
978
+ <div key={cpIdx} className={`provider-card ${isActive ? 'active-provider' : ''}`}>
979
+ <div className="provider-card-header">
980
+ <div className="provider-card-title">
981
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
982
+ <circle cx="12" cy="12" r="3" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14" />
983
+ </svg>
984
+ {cp.displayName || cp.id || 'Unnamed Provider'}
985
+ </div>
986
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
987
+ {isActive && <span className="provider-active-badge">Active</span>}
988
+ <button
989
+ className="btn btn-primary"
990
+ style={{ padding: '3px 8px', fontSize: '12px', background: savedProviderId === cp.id ? '#10b981' : '' }}
991
+ onClick={() => handleSaveProvider(cp.id)}
992
+ title="Save this provider settings"
993
+ >
994
+ {savedProviderId === cp.id ? 'Saved! ✓' : 'Save'}
995
+ </button>
996
+ <button
997
+ className="btn btn-danger"
998
+ style={{ padding: '3px 8px', fontSize: '12px' }}
999
+ onClick={deleteCp}
1000
+ title="Delete this provider"
1001
+ >
1002
+ Delete
1003
+ </button>
1004
+ </div>
1005
+ </div>
1006
+
1007
+ <div className="provider-card-body">
1008
+ <div className="setting-row">
1009
+ <label>Name</label>
1010
+ <input
1011
+ type="text"
1012
+ value={cp.displayName}
1013
+ onChange={(e) => {
1014
+ const name = e.target.value
1015
+ const computedId = name
1016
+ .toLowerCase()
1017
+ .replace(/[^a-z0-9_-]/g, '-')
1018
+ .replace(/-+/g, '-')
1019
+ .replace(/^-|-$/g, '') || `provider-${cpIdx + 1}`
1020
+ updateCp({ displayName: name, id: computedId })
1021
+ }}
1022
+ placeholder="e.g. DeepSeek"
1023
+ />
1024
+ </div>
1025
+
1026
+ <div className="setting-row">
1027
+ <label>Base URL</label>
1028
+ <input
1029
+ type="text"
1030
+ value={cp.baseUrl}
1031
+ onChange={(e) => updateCp({ baseUrl: e.target.value })}
1032
+ placeholder="https://api.myprovider.com/v1"
1033
+ />
1034
+ </div>
1035
+
1036
+ <div className="setting-row">
1037
+ <label>API Key</label>
1038
+ <input
1039
+ type="password"
1040
+ value={cp.apiKey}
1041
+ onChange={(e) => updateCp({ apiKey: e.target.value })}
1042
+ placeholder="Optional. Leave empty if you manage auth via headers."
1043
+ />
1044
+ </div>
1045
+
1046
+ {/* Models */}
1047
+ <div style={{ marginTop: '12px' }}>
1048
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Model list</label>
1049
+ {cp.models.map((m, mIdx) => (
1050
+ <div key={mIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1051
+ <input
1052
+ type="text"
1053
+ value={m.modelId}
1054
+ onChange={(e) => updateModel(mIdx, { modelId: e.target.value, displayName: e.target.value })}
1055
+ placeholder="e.g. llama-3.1-8b"
1056
+ style={{ flex: 1 }}
1057
+ />
1058
+ <button
1059
+ className="btn btn-danger"
1060
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1061
+ onClick={() => deleteModel(mIdx)}
1062
+ title="Remove model"
1063
+ >🗑</button>
1064
+ </div>
1065
+ ))}
1066
+ <button
1067
+ className="btn btn-secondary"
1068
+ style={{ fontSize: '12px', marginTop: '4px' }}
1069
+ onClick={addModel}
1070
+ >
1071
+ + Add model
1072
+ </button>
1073
+ </div>
1074
+
1075
+ {/* Headers */}
1076
+ <div style={{ marginTop: '12px' }}>
1077
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Headers (optional)</label>
1078
+ {cp.headers.map((h, hIdx) => (
1079
+ <div key={hIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1080
+ <input
1081
+ type="text"
1082
+ value={h.name}
1083
+ onChange={(e) => updateHeader(hIdx, { name: e.target.value })}
1084
+ placeholder="Header-Name"
1085
+ style={{ flex: 1 }}
1086
+ />
1087
+ <input
1088
+ type="text"
1089
+ value={h.value}
1090
+ onChange={(e) => updateHeader(hIdx, { value: e.target.value })}
1091
+ placeholder="value"
1092
+ style={{ flex: 1 }}
1093
+ />
1094
+ <button
1095
+ className="btn btn-danger"
1096
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1097
+ onClick={() => deleteHeader(hIdx)}
1098
+ title="Remove header"
1099
+ >🗑</button>
1100
+ </div>
1101
+ ))}
1102
+ <button
1103
+ className="btn btn-secondary"
1104
+ style={{ fontSize: '12px', marginTop: '4px' }}
1105
+ onClick={addHeader}
1106
+ >
1107
+ + Add header
1108
+ </button>
1109
+ </div>
1110
+ </div>
1111
+ </div>
1112
+ )
1113
+ })}
1114
+ </div>
1115
+
1116
+ <div className="setting-actions" style={{ marginTop: '16px' }}>
1117
+ <button
1118
+ className="btn btn-secondary"
1119
+ onClick={() => {
1120
+ const newId = `provider-${(config.customProviders ?? []).length + 1}`
1121
+ updateField('customProviders', [
1122
+ ...(config.customProviders ?? []),
1123
+ { id: newId, displayName: '', baseUrl: '', apiKey: '', models: [], headers: [] }
1124
+ ])
1125
+ }}
1126
+ >
1127
+ + Add custom provider
1128
+ </button>
1129
+ </div>
1130
+ </section>
1131
+
880
1132
  {isDesktopApp && (
881
1133
  <section className="setting-section">
882
1134
  <div className="section-heading">
883
1135
  <div>
884
1136
  <p className="section-kicker">Desktop updates</p>
885
-
886
1137
  <h2 className="section-title">Signed Tauri Channel</h2>
887
1138
  </div>
888
1139
  <p className="section-description">Check and explicitly install signed Tauri releases from your configured update channel.</p>
@@ -8,6 +8,27 @@ import ThemeTab from './Settings/ThemeTab'
8
8
  import PluginsTab from './Settings/PluginsTab'
9
9
  import AgentsTab from './Settings/AgentsTab'
10
10
 
11
+ // ─── Custom Provider Types ────────────────────────────────────────────────────
12
+
13
+ export interface CustomProviderModel {
14
+ modelId: string
15
+ displayName: string
16
+ }
17
+
18
+ export interface CustomProviderHeader {
19
+ name: string
20
+ value: string
21
+ }
22
+
23
+ export interface CustomProviderConfig {
24
+ id: string
25
+ displayName: string
26
+ baseUrl: string
27
+ apiKey: string
28
+ models: CustomProviderModel[]
29
+ headers: CustomProviderHeader[]
30
+ }
31
+
11
32
  export const DEFAULT_CONFIG = {
12
33
  theme: 'dark',
13
34
  accentColor: '#10b981',
@@ -122,7 +143,11 @@ export const DEFAULT_CONFIG = {
122
143
  systemInstruction: '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.',
123
144
  enabled: true
124
145
  }
125
- ]
146
+ ],
147
+ // Custom Providers
148
+ customProviders: [] as CustomProviderConfig[],
149
+ // Per-provider model selection: { [providerId]: selectedModelId }
150
+ customModelSelections: {} as Record<string, string>,
126
151
  }
127
152
 
128
153
  type TabType = 'sect-general' | 'sect-audio' | 'sect-automation' | 'sect-theme' | 'sect-plugins' | 'sect-shortcuts' | 'sect-memory' | 'sect-agents'
@@ -410,6 +435,54 @@ export default function SettingsWindow() {
410
435
  }
411
436
  }
412
437
 
438
+ const handleSaveWithoutClosing = async () => {
439
+ const finalConfig = { ...config }
440
+
441
+ if (config.geminiModel === 'custom') {
442
+ finalConfig.geminiModel = customGemini || 'gemini-2.5-flash'
443
+ }
444
+ if (config.openaiModel === 'custom') {
445
+ finalConfig.openaiModel = customOpenAI || 'gpt-4o'
446
+ }
447
+ if (config.openrouterModel === 'custom') {
448
+ finalConfig.openrouterModel = customOpenRouter || 'openai/gpt-4o-mini'
449
+ }
450
+ if (config.deepseekModel === 'custom') {
451
+ finalConfig.deepseekModel = customDeepSeek || 'deepseek-v4-flash'
452
+ }
453
+ if (config.anthropicModel === 'custom') {
454
+ finalConfig.anthropicModel = customAnthropic || 'claude-3-5-sonnet-latest'
455
+ }
456
+ if (config.hfModel === 'custom') {
457
+ finalConfig.hfModel = customHF || 'meta-llama/Meta-Llama-3-8B-Instruct'
458
+ }
459
+ if (config.localModelName === 'custom') {
460
+ finalConfig.localModelName = customLocal || 'local-model'
461
+ }
462
+ if (config.ollamaModel === 'custom') {
463
+ finalConfig.ollamaModel = customOllama || 'llama3:latest'
464
+ }
465
+
466
+ try {
467
+ await setProfileValue('name', userName)
468
+ await setProfileValue('preferences', userPreferences)
469
+ } catch (e) {
470
+ console.error("Failed to save user profile memory:", e)
471
+ }
472
+
473
+ if (window.settingsApi) {
474
+ await window.settingsApi.saveSettings(finalConfig)
475
+ applyThemeStyles(finalConfig)
476
+ } else {
477
+ try {
478
+ await setProfileValue('user-settings', JSON.stringify(finalConfig))
479
+ applyThemeStyles(finalConfig)
480
+ } catch (e) {
481
+ console.error("Failed to save user settings:", e)
482
+ }
483
+ }
484
+ }
485
+
413
486
  const handleReset = async () => {
414
487
  if (confirm('Reset all settings to default?')) {
415
488
  setConfig(DEFAULT_CONFIG)
@@ -694,6 +767,7 @@ export default function SettingsWindow() {
694
767
  handleCheckUpdates={handleCheckUpdates}
695
768
  handleInstallUpdate={handleInstallUpdate}
696
769
  isDesktopApp={isDesktopApp}
770
+ onSaveWithoutClosing={handleSaveWithoutClosing}
697
771
  />
698
772
  )}
699
773
 
@@ -1683,12 +1683,28 @@ h1 {
1683
1683
  width: 15px;
1684
1684
  }
1685
1685
 
1686
+ .agent-activity-icon[data-has-img="true"]::before,
1687
+ .agent-activity-icon[data-has-img="true"]::after {
1688
+ display: none !important;
1689
+ }
1690
+
1691
+ .agent-activity-icon img {
1692
+ width: 14px;
1693
+ height: 14px;
1694
+ display: block;
1695
+ object-fit: contain;
1696
+ filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.12));
1697
+ pointer-events: none;
1698
+ user-select: none;
1699
+ }
1700
+
1686
1701
  .agent-activity-icon::before,
1687
1702
  .agent-activity-icon::after {
1688
1703
  content: "";
1689
1704
  position: absolute;
1690
1705
  }
1691
1706
 
1707
+
1692
1708
  .agent-activity-item[data-kind="folder"] .agent-activity-icon::before {
1693
1709
  background: var(--text-muted);
1694
1710
  border-radius: 2px 2px 1px 1px;
@@ -201,28 +201,30 @@ pub fn capture_screen() -> Result<String, String> {
201
201
  }
202
202
 
203
203
  pub fn read_clipboard_image() -> Result<String, String> {
204
- let output = Command::new("wl-paste").args(&["-t", "image/png"]).output();
204
+ let output = Command::new("wl-paste").args(["-t", "image/png"]).output();
205
205
 
206
- if let Ok(out) = output {
207
- if out.status.success() && !out.stdout.is_empty() {
208
- return Ok(format!(
209
- "data:image/png;base64,{}",
210
- STANDARD.encode(&out.stdout)
211
- ));
212
- }
206
+ if let Ok(out) = output
207
+ && out.status.success()
208
+ && !out.stdout.is_empty()
209
+ {
210
+ return Ok(format!(
211
+ "data:image/png;base64,{}",
212
+ STANDARD.encode(&out.stdout)
213
+ ));
213
214
  }
214
215
 
215
216
  let output = Command::new("xclip")
216
- .args(&["-selection", "clipboard", "-t", "image/png", "-o"])
217
+ .args(["-selection", "clipboard", "-t", "image/png", "-o"])
217
218
  .output();
218
219
 
219
- if let Ok(out) = output {
220
- if out.status.success() && !out.stdout.is_empty() {
221
- return Ok(format!(
222
- "data:image/png;base64,{}",
223
- STANDARD.encode(&out.stdout)
224
- ));
225
- }
220
+ if let Ok(out) = output
221
+ && out.status.success()
222
+ && !out.stdout.is_empty()
223
+ {
224
+ return Ok(format!(
225
+ "data:image/png;base64,{}",
226
+ STANDARD.encode(&out.stdout)
227
+ ));
226
228
  }
227
229
 
228
230
  Err("No image found in clipboard".into())
@@ -14,7 +14,7 @@ pub fn set_activity(config: &MintConfig, instruction: &str) -> Result<String, St
14
14
  .filter(|value| !value.trim().is_empty())
15
15
  .ok_or("missing config value 'discordApplicationId'")?;
16
16
  let input: Value = serde_json::from_str(instruction).unwrap_or_else(|_| {
17
- json!({ "details": instruction.trim().is_empty().then_some("Using Mint Assistant").unwrap_or(instruction) })
17
+ json!({ "details": if instruction.trim().is_empty() { "Using Mint Assistant" } else { instruction } })
18
18
  });
19
19
  let activity = json!({
20
20
  "details": input["details"].as_str().unwrap_or("Using Mint Assistant"),
@@ -119,11 +119,7 @@ fn get_runtime_status() -> Result<RuntimeStatus, String> {
119
119
  .display()
120
120
  .to_string(),
121
121
  active_provider: config.ai_provider.clone(),
122
- available_providers: config
123
- .available_providers()
124
- .into_iter()
125
- .map(str::to_owned)
126
- .collect(),
122
+ available_providers: config.available_providers(),
127
123
  integrations: integration_status(&config),
128
124
  })
129
125
  }
@@ -331,19 +327,27 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
331
327
 
332
328
  let abort_handle = join_handle.abort_handle();
333
329
  if !chat_id_str.is_empty() {
334
- mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
330
+ mint_core::ACTIVE_AGENTS
331
+ .lock()
332
+ .unwrap()
333
+ .insert(chat_id_str.clone(), abort_handle);
335
334
  }
336
335
 
337
336
  let res = join_handle.await;
338
337
 
339
338
  if !chat_id_str.is_empty() {
340
- mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
339
+ mint_core::ACTIVE_AGENTS
340
+ .lock()
341
+ .unwrap()
342
+ .remove(&chat_id_str);
341
343
  }
342
344
 
343
345
  let (response, _) = match res {
344
346
  Ok(Ok(val)) => val,
345
347
  Ok(Err(e)) => return Err(e.to_string()),
346
- Err(e) if e.is_cancelled() => return Err("Chat execution cancelled by user".to_string()),
348
+ Err(e) if e.is_cancelled() => {
349
+ return Err("Chat execution cancelled by user".to_string());
350
+ }
347
351
  Err(e) => return Err(format!("Task panicked: {}", e)),
348
352
  };
349
353
  return Ok(response);
@@ -409,13 +413,19 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
409
413
 
410
414
  let abort_handle = join_handle.abort_handle();
411
415
  if !chat_id_str.is_empty() {
412
- mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
416
+ mint_core::ACTIVE_AGENTS
417
+ .lock()
418
+ .unwrap()
419
+ .insert(chat_id_str.clone(), abort_handle);
413
420
  }
414
421
 
415
422
  let res = join_handle.await;
416
423
 
417
424
  if !chat_id_str.is_empty() {
418
- mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
425
+ mint_core::ACTIVE_AGENTS
426
+ .lock()
427
+ .unwrap()
428
+ .remove(&chat_id_str);
419
429
  }
420
430
 
421
431
  let res = match res {
@@ -458,19 +468,27 @@ async fn stream_chat_message(
458
468
 
459
469
  let abort_handle = join_handle.abort_handle();
460
470
  if !chat_id_str.is_empty() {
461
- mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
471
+ mint_core::ACTIVE_AGENTS
472
+ .lock()
473
+ .unwrap()
474
+ .insert(chat_id_str.clone(), abort_handle);
462
475
  }
463
476
 
464
477
  let res = join_handle.await;
465
478
 
466
479
  if !chat_id_str.is_empty() {
467
- mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
480
+ mint_core::ACTIVE_AGENTS
481
+ .lock()
482
+ .unwrap()
483
+ .remove(&chat_id_str);
468
484
  }
469
485
 
470
486
  let (response, _) = match res {
471
487
  Ok(Ok(val)) => val,
472
488
  Ok(Err(e)) => return Err(e.to_string()),
473
- Err(e) if e.is_cancelled() => return Err("Chat execution cancelled by user".to_string()),
489
+ Err(e) if e.is_cancelled() => {
490
+ return Err("Chat execution cancelled by user".to_string());
491
+ }
474
492
  Err(e) => return Err(format!("Task panicked: {}", e)),
475
493
  };
476
494
  return Ok(response);
@@ -551,13 +569,19 @@ async fn stream_chat_message(
551
569
 
552
570
  let abort_handle = join_handle.abort_handle();
553
571
  if !chat_id_str.is_empty() {
554
- mint_core::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
572
+ mint_core::ACTIVE_AGENTS
573
+ .lock()
574
+ .unwrap()
575
+ .insert(chat_id_str.clone(), abort_handle);
555
576
  }
556
577
 
557
578
  let res = join_handle.await;
558
579
 
559
580
  if !chat_id_str.is_empty() {
560
- mint_core::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str);
581
+ mint_core::ACTIVE_AGENTS
582
+ .lock()
583
+ .unwrap()
584
+ .remove(&chat_id_str);
561
585
  }
562
586
 
563
587
  let res = match res {
@@ -1157,8 +1181,8 @@ fn install_tray(app: &AppHandle) -> tauri::Result<()> {
1157
1181
  fn install_shortcuts(app: &AppHandle) -> tauri::Result<()> {
1158
1182
  let main_shortcut = Shortcut::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::Space);
1159
1183
  let spotlight_shortcut = Shortcut::new(Some(Modifiers::ALT), Code::Space);
1160
- let main_handler = main_shortcut.clone();
1161
- let spotlight_handler = spotlight_shortcut.clone();
1184
+ let main_handler = main_shortcut;
1185
+ let spotlight_handler = spotlight_shortcut;
1162
1186
  app.plugin(
1163
1187
  tauri_plugin_global_shortcut::Builder::new()
1164
1188
  .with_handler(move |app, shortcut, event| {