@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.
- package/Cargo.toml +1 -1
- package/Release_Note.md +48 -3
- package/crates/mint-cli/src/agent.rs +95 -84
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/main.rs +806 -403
- package/crates/mint-cli/src/onboard.rs +108 -108
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-core/src/api_server.rs +57 -54
- package/crates/mint-core/src/browser.rs +1 -1
- package/crates/mint-core/src/chat.rs +123 -8
- package/crates/mint-core/src/code_tools.rs +18 -20
- package/crates/mint-core/src/config.rs +184 -17
- package/crates/mint-core/src/image_gen.rs +5 -5
- package/crates/mint-core/src/lib.rs +5 -5
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +104 -85
- package/crates/mint-core/src/pictures.rs +3 -4
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +46 -19
- package/package.json +1 -1
- package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
- package/shared-modules/linux-audio/fluidsynth2.json +8 -2
- package/shared-modules/linux-audio/lilv.json +26 -19
- package/shared-modules/linux-audio/lv2.json +0 -1
- package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
- package/src/renderer/shared/utils/fileIcons.ts +135 -0
- package/src/renderer/src/components/ChatPanel.tsx +25 -5
- package/src/renderer/src/components/MintDashboard.tsx +45 -2
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
- package/src/renderer/src/components/SettingsWindow.tsx +68 -1
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/styles.css +16 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +25 -5
- package/src/renderer/src-web/components/MintDashboard.tsx +19 -1
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
- package/src/renderer/src-web/components/SettingsWindow.tsx +75 -1
- package/src/renderer/src-web/css/styles.css +16 -0
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +41 -17
- 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;
|
package/src-tauri/src/desktop.rs
CHANGED
|
@@ -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(
|
|
204
|
+
let output = Command::new("wl-paste").args(["-t", "image/png"]).output();
|
|
205
205
|
|
|
206
|
-
if let Ok(out) = output
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
217
|
+
.args(["-selection", "clipboard", "-t", "image/png", "-o"])
|
|
217
218
|
.output();
|
|
218
219
|
|
|
219
|
-
if let Ok(out) = output
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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()
|
|
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"),
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -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
|
|
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
|
|
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() =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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() =>
|
|
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
|
|
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
|
|
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
|
|
1161
|
-
let spotlight_handler = spotlight_shortcut
|
|
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| {
|