@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
@@ -8,17 +8,32 @@
8
8
  "sources": [
9
9
  {
10
10
  "type": "archive",
11
- "url": "https://download.drobilla.net/serd-0.30.16.tar.xz",
12
- "sha256": "f50f486da519cdd8d03b20c9e42414e459133f5a244411d8e63caef8d9ac9146"
11
+ "url": "https://download.drobilla.net/serd-0.32.10.tar.xz",
12
+ "sha256": "b0e93b49e52f01a049475b7886ef140407115a32d3b1e5dc5f95141c88275d1c"
13
13
  }
14
14
  ],
15
- "post-install": [
16
- "install -Dm644 -t $FLATPAK_DEST/share/licenses/serd ../COPYING"
15
+ "cleanup": [
16
+ "/bin",
17
+ "/include",
18
+ "/lib/pkgconfig",
19
+ "/share/man"
20
+ ]
21
+ },
22
+ {
23
+ "name": "zix",
24
+ "buildsystem": "meson",
25
+ "sources": [
26
+ {
27
+ "type": "archive",
28
+ "url": "https://download.drobilla.net/zix-0.8.2.tar.xz",
29
+ "sha256": "4c73aab0f8cbbfe56b00c8c6d648316021e699c5ae6cbf254391ef309047e67b"
30
+ }
17
31
  ],
18
32
  "cleanup": [
19
33
  "/bin",
20
34
  "/include",
21
35
  "/lib/pkgconfig",
36
+ "/share/doc",
22
37
  "/share/man"
23
38
  ]
24
39
  },
@@ -28,13 +43,10 @@
28
43
  "sources": [
29
44
  {
30
45
  "type": "archive",
31
- "url": "https://download.drobilla.net/sord-0.16.14.tar.xz",
32
- "sha256": "220fd97d5fcb216e7b85db66f685bfdaad7dc58a50d1f96dfb2558dbc6c4731b"
46
+ "url": "https://download.drobilla.net/sord-0.16.22.tar.xz",
47
+ "sha256": "bb23b34b216579136795d518cffa73d91cf205594ce9accebfd408afb839173f"
33
48
  }
34
49
  ],
35
- "post-install": [
36
- "install -Dm644 -t $FLATPAK_DEST/share/licenses/sord ../COPYING"
37
- ],
38
50
  "cleanup": [
39
51
  "/bin",
40
52
  "/include",
@@ -43,6 +55,7 @@
43
55
  "/share/man"
44
56
  ]
45
57
  },
58
+ "lv2.json",
46
59
  {
47
60
  "name": "sratom",
48
61
  "buildsystem": "meson",
@@ -53,25 +66,19 @@
53
66
  "sources": [
54
67
  {
55
68
  "type": "archive",
56
- "url": "https://download.drobilla.net/sratom-0.6.14.tar.xz",
57
- "sha256": "9982faf40db83aedd9b3850e499fecd6852b8b4ba6dede514013655cffaca1e6"
69
+ "url": "https://download.drobilla.net/sratom-0.6.22.tar.xz",
70
+ "sha256": "0209b7d0f22c96abb416722ed735b0933be47931ecff4aa4b26ded7760b4f252"
58
71
  }
59
- ],
60
- "post-install": [
61
- "install -Dm644 -t $FLATPAK_DEST/share/licenses/sratom ../COPYING"
62
72
  ]
63
73
  }
64
74
  ],
65
75
  "sources": [
66
76
  {
67
77
  "type": "archive",
68
- "url": "https://download.drobilla.net/lilv-0.24.20.tar.xz",
69
- "sha256": "4fb082b9b8b286ea92bbb71bde6b75624cecab6df0cc639ee75a2a096212eebc"
78
+ "url": "https://download.drobilla.net/lilv-0.28.0.tar.xz",
79
+ "sha256": "8dcb70adb5cf072335115a6b091f4113710bdc73abaadaa3f9e9c1e55957b149"
70
80
  }
71
81
  ],
72
- "post-install": [
73
- "install -Dm644 -t $FLATPAK_DEST/share/licenses/lilv ../COPYING"
74
- ],
75
82
  "cleanup": [
76
83
  "/bin",
77
84
  "/etc",
@@ -15,7 +15,6 @@
15
15
  }
16
16
  ],
17
17
  "post-install": [
18
- "install -Dm644 -t $FLATPAK_DEST/share/licenses/lv2 ../COPYING",
19
18
  "ln -sf lv2.pc $FLATPAK_DEST/lib/pkgconfig/lv2core.pc"
20
19
  ]
21
20
  }
@@ -1,15 +1,30 @@
1
- /**
2
- * shared/components/AgentActivityTable.tsx
3
- * Renders the "Working through task" activity drawer rows.
4
- * Shared by both Desktop and Web ChatPanel — do NOT duplicate this.
5
- */
6
1
  import { useState } from 'react'
7
2
  import type { AgentActivityView } from '../utils/agentActivity'
3
+ import { materialFileIcon, materialFolderIcon, getExtension } from '../utils/fileIcons'
8
4
 
9
5
  interface Props {
10
6
  activityView: AgentActivityView
11
7
  }
12
8
 
9
+ function getFilename(target: string): string {
10
+ const clean = target.split(' #')[0].trim()
11
+ const segments = clean.split('/')
12
+ return segments[segments.length - 1]
13
+ }
14
+
15
+ function resolveActivityIcon(kind: string, target: string): string | null {
16
+ if (kind === 'file') {
17
+ const filename = getFilename(target)
18
+ const ext = getExtension(filename)
19
+ return materialFileIcon(filename, ext)
20
+ }
21
+ if (kind === 'folder') {
22
+ const foldername = getFilename(target)
23
+ return materialFolderIcon(foldername, false)
24
+ }
25
+ return null
26
+ }
27
+
13
28
  export function AgentActivityTable({ activityView }: Props) {
14
29
  const [expandedIndices, setExpandedIndices] = useState<Record<number, boolean>>({})
15
30
 
@@ -30,6 +45,7 @@ export function AgentActivityTable({ activityView }: Props) {
30
45
  </div>
31
46
  {activityView.items.map((activity, index) => {
32
47
  const isExpanded = !!expandedIndices[index]
48
+ const icon = resolveActivityIcon(activity.kind, activity.target)
33
49
  return (
34
50
  <div
35
51
  className="agent-activity-item"
@@ -40,7 +56,9 @@ export function AgentActivityTable({ activityView }: Props) {
40
56
  onClick={() => toggleExpand(index)}
41
57
  >
42
58
  <span className="agent-activity-label">{activity.label}</span>
43
- <span className="agent-activity-icon" aria-hidden="true" />
59
+ <span className="agent-activity-icon" aria-hidden="true" data-has-img={!!icon}>
60
+ {icon && <img src={icon} alt="" draggable={false} />}
61
+ </span>
44
62
  <span
45
63
  className="agent-activity-text"
46
64
  style={isExpanded ? { whiteSpace: 'normal', wordBreak: 'break-all', overflow: 'visible', textOverflow: 'clip' } : undefined}
@@ -64,3 +82,4 @@ export function AgentActivityTable({ activityView }: Props) {
64
82
  </div>
65
83
  )
66
84
  }
85
+
@@ -0,0 +1,135 @@
1
+ /**
2
+ * shared/utils/fileIcons.ts
3
+ * Utility mapping and resolution functions for Material Icon Theme icons.
4
+ * Extracted from WorkspacePanel to be shared across multiple components (e.g. AgentActivityTable).
5
+ */
6
+
7
+ import folderIcon from 'material-icon-theme/icons/folder.svg?url'
8
+ import folderOpenIcon from 'material-icon-theme/icons/folder-open.svg?url'
9
+ import folderComponentsIcon from 'material-icon-theme/icons/folder-components.svg?url'
10
+ import folderComponentsOpenIcon from 'material-icon-theme/icons/folder-components-open.svg?url'
11
+ import folderConfigIcon from 'material-icon-theme/icons/folder-config.svg?url'
12
+ import folderConfigOpenIcon from 'material-icon-theme/icons/folder-config-open.svg?url'
13
+ import folderDistIcon from 'material-icon-theme/icons/folder-dist.svg?url'
14
+ import folderDistOpenIcon from 'material-icon-theme/icons/folder-dist-open.svg?url'
15
+ import folderDocsIcon from 'material-icon-theme/icons/folder-docs.svg?url'
16
+ import folderDocsOpenIcon from 'material-icon-theme/icons/folder-docs-open.svg?url'
17
+ import folderGitIcon from 'material-icon-theme/icons/folder-git.svg?url'
18
+ import folderGitOpenIcon from 'material-icon-theme/icons/folder-git-open.svg?url'
19
+ import folderGithubIcon from 'material-icon-theme/icons/folder-github.svg?url'
20
+ import folderGithubOpenIcon from 'material-icon-theme/icons/folder-github-open.svg?url'
21
+ import folderImagesIcon from 'material-icon-theme/icons/folder-images.svg?url'
22
+ import folderImagesOpenIcon from 'material-icon-theme/icons/folder-images-open.svg?url'
23
+ import folderLogIcon from 'material-icon-theme/icons/folder-log.svg?url'
24
+ import folderLogOpenIcon from 'material-icon-theme/icons/folder-log-open.svg?url'
25
+ import folderNodeIcon from 'material-icon-theme/icons/folder-node.svg?url'
26
+ import folderNodeOpenIcon from 'material-icon-theme/icons/folder-node-open.svg?url'
27
+ import folderPublicIcon from 'material-icon-theme/icons/folder-public.svg?url'
28
+ import folderPublicOpenIcon from 'material-icon-theme/icons/folder-public-open.svg?url'
29
+ import folderRustIcon from 'material-icon-theme/icons/folder-rust.svg?url'
30
+ import folderRustOpenIcon from 'material-icon-theme/icons/folder-rust-open.svg?url'
31
+ import folderScriptsIcon from 'material-icon-theme/icons/folder-scripts.svg?url'
32
+ import folderScriptsOpenIcon from 'material-icon-theme/icons/folder-scripts-open.svg?url'
33
+ import folderSrcIcon from 'material-icon-theme/icons/folder-src.svg?url'
34
+ import folderSrcOpenIcon from 'material-icon-theme/icons/folder-src-open.svg?url'
35
+ import folderSrcTauriIcon from 'material-icon-theme/icons/folder-src-tauri.svg?url'
36
+ import folderSrcTauriOpenIcon from 'material-icon-theme/icons/folder-src-tauri-open.svg?url'
37
+ import folderTestIcon from 'material-icon-theme/icons/folder-test.svg?url'
38
+ import folderTestOpenIcon from 'material-icon-theme/icons/folder-test-open.svg?url'
39
+ import folderUiIcon from 'material-icon-theme/icons/folder-ui.svg?url'
40
+ import folderUiOpenIcon from 'material-icon-theme/icons/folder-ui-open.svg?url'
41
+ import folderUtilsIcon from 'material-icon-theme/icons/folder-utils.svg?url'
42
+ import folderUtilsOpenIcon from 'material-icon-theme/icons/folder-utils-open.svg?url'
43
+ import cssIcon from 'material-icon-theme/icons/css.svg?url'
44
+ import documentIcon from 'material-icon-theme/icons/document.svg?url'
45
+ import htmlIcon from 'material-icon-theme/icons/html.svg?url'
46
+ import imageIcon from 'material-icon-theme/icons/image.svg?url'
47
+ import javascriptIcon from 'material-icon-theme/icons/javascript.svg?url'
48
+ import jsonIcon from 'material-icon-theme/icons/json.svg?url'
49
+ import lockIcon from 'material-icon-theme/icons/lock.svg?url'
50
+ import logIcon from 'material-icon-theme/icons/log.svg?url'
51
+ import markdownIcon from 'material-icon-theme/icons/markdown.svg?url'
52
+ import npmIcon from 'material-icon-theme/icons/npm.svg?url'
53
+ import reactIcon from 'material-icon-theme/icons/react.svg?url'
54
+ import reactTsIcon from 'material-icon-theme/icons/react_ts.svg?url'
55
+ import rustIcon from 'material-icon-theme/icons/rust.svg?url'
56
+ import settingsIcon from 'material-icon-theme/icons/settings.svg?url'
57
+ import tauriIcon from 'material-icon-theme/icons/tauri.svg?url'
58
+ import tomlIcon from 'material-icon-theme/icons/toml.svg?url'
59
+ import typescriptIcon from 'material-icon-theme/icons/typescript.svg?url'
60
+ import viteIcon from 'material-icon-theme/icons/vite.svg?url'
61
+ import yamlIcon from 'material-icon-theme/icons/yaml.svg?url'
62
+
63
+ export const FOLDER_ICONS: Record<string, { closed: string; open: string }> = {
64
+ '.cargo_home': { closed: folderRustIcon, open: folderRustOpenIcon },
65
+ '.github': { closed: folderGithubIcon, open: folderGithubOpenIcon },
66
+ '.git': { closed: folderGitIcon, open: folderGitOpenIcon },
67
+ '.rustup': { closed: folderRustIcon, open: folderRustOpenIcon },
68
+ '.rustup_copy': { closed: folderRustIcon, open: folderRustOpenIcon },
69
+ '.rustup_home': { closed: folderRustIcon, open: folderRustOpenIcon },
70
+ assets: { closed: folderImagesIcon, open: folderImagesOpenIcon },
71
+ components: { closed: folderComponentsIcon, open: folderComponentsOpenIcon },
72
+ crates: { closed: folderRustIcon, open: folderRustOpenIcon },
73
+ css: { closed: folderUiIcon, open: folderUiOpenIcon },
74
+ dist: { closed: folderDistIcon, open: folderDistOpenIcon },
75
+ docs: { closed: folderDocsIcon, open: folderDocsOpenIcon },
76
+ logs: { closed: folderLogIcon, open: folderLogOpenIcon },
77
+ node_modules: { closed: folderNodeIcon, open: folderNodeOpenIcon },
78
+ out: { closed: folderDistIcon, open: folderDistOpenIcon },
79
+ public: { closed: folderPublicIcon, open: folderPublicOpenIcon },
80
+ renderer: { closed: folderUiIcon, open: folderUiOpenIcon },
81
+ scripts: { closed: folderScriptsIcon, open: folderScriptsOpenIcon },
82
+ src: { closed: folderSrcIcon, open: folderSrcOpenIcon },
83
+ 'src-tauri': { closed: folderSrcTauriIcon, open: folderSrcTauriOpenIcon },
84
+ tests: { closed: folderTestIcon, open: folderTestOpenIcon },
85
+ utils: { closed: folderUtilsIcon, open: folderUtilsOpenIcon },
86
+ }
87
+
88
+ export const FILE_ICONS_BY_EXTENSION: Record<string, string> = {
89
+ css: cssIcon,
90
+ html: htmlIcon,
91
+ jpeg: imageIcon,
92
+ jpg: imageIcon,
93
+ js: javascriptIcon,
94
+ json: jsonIcon,
95
+ lock: lockIcon,
96
+ log: logIcon,
97
+ md: markdownIcon,
98
+ png: imageIcon,
99
+ rs: rustIcon,
100
+ svg: imageIcon,
101
+ toml: tomlIcon,
102
+ ts: typescriptIcon,
103
+ tsx: reactTsIcon,
104
+ yaml: yamlIcon,
105
+ yml: yamlIcon,
106
+ }
107
+
108
+ export const FILE_ICONS_BY_NAME: Record<string, string> = {
109
+ 'package-lock.json': npmIcon,
110
+ 'package.json': npmIcon,
111
+ 'tauri.conf.json': tauriIcon,
112
+ 'vite.config.ts': viteIcon,
113
+ 'vite.config.web.ts': viteIcon,
114
+ }
115
+
116
+ export function getExtension(name: string): string {
117
+ const index = name.lastIndexOf('.')
118
+ return index === -1 ? '' : name.slice(index + 1).toLowerCase()
119
+ }
120
+
121
+ export function materialFolderIcon(name: string, open: boolean): string {
122
+ const icon = FOLDER_ICONS[name] || FOLDER_ICONS[name.toLowerCase()]
123
+ if (!icon) return open ? folderOpenIcon : folderIcon
124
+ return open ? icon.open : icon.closed
125
+ }
126
+
127
+ export function materialFileIcon(name: string, fileExtension: string): string {
128
+ if (name.endsWith('.config.ts') || name.endsWith('.config.js')) return settingsIcon
129
+ if (name.endsWith('.tsx')) return reactTsIcon
130
+ if (name.endsWith('.jsx')) return reactIcon
131
+ return FILE_ICONS_BY_NAME[name.toLowerCase()] || FILE_ICONS_BY_EXTENSION[fileExtension] || documentIcon
132
+ }
133
+
134
+ export { folderIcon, folderOpenIcon }
135
+
@@ -224,8 +224,13 @@ export default function ChatPanel({
224
224
  case 'ollama':
225
225
  return dynamicOllamaModels
226
226
  default:
227
+ if (provider.startsWith('custom:')) {
228
+ const id = provider.replace(/^custom:/, '')
229
+ const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
230
+ return cp?.models.map(m => m.modelId) ?? []
231
+ }
227
232
  return []
228
- }
233
+ }
229
234
  }
230
235
 
231
236
  const activeProvider = status?.activeProvider ?? ''
@@ -251,6 +256,11 @@ export default function ChatPanel({
251
256
  case 'ollama':
252
257
  return settingsConfig.ollamaModel
253
258
  default:
259
+ if (provider.startsWith('custom:')) {
260
+ const id = provider.replace(/^custom:/, '')
261
+ const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
262
+ return (settingsConfig.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
263
+ }
254
264
  return ''
255
265
  }
256
266
  }
@@ -382,14 +392,19 @@ export default function ChatPanel({
382
392
  }
383
393
  }
384
394
  if (sending) return
385
- if (!settingsConfig?.enableVoiceReply && !voiceMode) {
386
- lastAutoSpokenIdRef.current = latest?.id ?? null
395
+ if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
396
+
397
+ if (!settingsConfig?.enableVoiceReply) {
398
+ lastAutoSpokenIdRef.current = latest.id
399
+ if (voiceMode) {
400
+ scheduleVoiceListen(350)
401
+ }
387
402
  return
388
403
  }
389
- if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
404
+
390
405
  lastAutoSpokenIdRef.current = latest.id
391
406
  speak(latest.aiText)
392
- }, [interactions, sending, settingsConfig?.enableVoiceReply])
407
+ }, [interactions, sending, settingsConfig?.enableVoiceReply, voiceMode])
393
408
  useEffect(() => {
394
409
  if (!toolMenuOpen) return
395
410
  const closeMenu = (event: MouseEvent) => {
@@ -1055,6 +1070,11 @@ export default function ChatPanel({
1055
1070
  else if (provider === 'huggingface') displayName = 'HF'
1056
1071
  else if (provider === 'local_openai') displayName = 'Local'
1057
1072
  else if (provider === 'ollama') displayName = 'Ollama'
1073
+ else if (provider.startsWith('custom:')) {
1074
+ const id = provider.replace(/^custom:/, '')
1075
+ const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
1076
+ displayName = cp?.displayName || id
1077
+ }
1058
1078
  return <option key={provider} value={provider}>{displayName}</option>
1059
1079
  })}
1060
1080
  </select>
@@ -15,6 +15,7 @@ import {
15
15
  cancelChatMessage,
16
16
  submitToolApproval,
17
17
  listen,
18
+ readClipboardImage,
18
19
  type AgentProgress,
19
20
  type ChatResponse,
20
21
  type ChatSession,
@@ -22,6 +23,7 @@ import {
22
23
  type PictureEntry,
23
24
  type RuntimeStatus,
24
25
  } from '../tauri'
26
+
25
27
  import ChatPanel from './ChatPanel'
26
28
  import DashboardSidebar, { type DashboardView } from './DashboardSidebar'
27
29
  import ImageStudioPanel from './ImageStudioPanel'
@@ -138,6 +140,7 @@ export default function MintDashboard() {
138
140
  const [conversationId, setConversationId] = useState(activeConversationId)
139
141
  const [chatSessions, setChatSessions] = useState<ChatSession[]>([])
140
142
  const chatEnd = useRef<HTMLDivElement | null>(null)
143
+ const lastNativePasteTimeRef = useRef(0)
141
144
  const startupReady = (dashboardDataReady && modelReady) || startupTimedOut
142
145
  const [proactiveSuggestion, setProactiveSuggestion] = useState<any>(null)
143
146
 
@@ -575,7 +578,28 @@ export default function MintDashboard() {
575
578
  }
576
579
  }
577
580
 
578
- if (!file) return false
581
+ if (!file) {
582
+ // Fallback for Tauri desktop app where WebKitGTK/WebView clipboard paste events do not provide file handles
583
+ const now = Date.now()
584
+ if (now - lastNativePasteTimeRef.current > 100) {
585
+ lastNativePasteTimeRef.current = now
586
+ readClipboardImage().then((dataUri) => {
587
+ if (dataUri) {
588
+ const name = 'Pasted image'
589
+ createTrimmedImagePreview(dataUri)
590
+ .catch(() => dataUri)
591
+ .then((previewDataUri) => {
592
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name }])
593
+ })
594
+ }
595
+ }).catch((err) => {
596
+ console.warn('Failed to read clipboard image via Tauri API:', err)
597
+ })
598
+ }
599
+ return false
600
+ }
601
+
602
+
579
603
 
580
604
  readImage(file)
581
605
  .then((dataUri) => {
@@ -591,6 +615,7 @@ export default function MintDashboard() {
591
615
  }
592
616
 
593
617
 
618
+
594
619
  async function selectDocument(event: ChangeEvent<HTMLInputElement>) {
595
620
  const file = event.target.files?.[0]
596
621
  if (!file) return
@@ -736,6 +761,11 @@ export default function MintDashboard() {
736
761
  else if (provider === 'huggingface') providerName = 'HF'
737
762
  else if (provider === 'local_openai') providerName = 'Local'
738
763
  else if (provider === 'ollama') providerName = 'Ollama'
764
+ else if (provider.startsWith('custom:')) {
765
+ const id = provider.replace(/^custom:/, '')
766
+ const cp = (settingsConfig?.customProviders ?? []).find((p: any) => p.id === id)
767
+ providerName = cp?.displayName || id
768
+ }
739
769
 
740
770
  if (providerName && providerName === provider) {
741
771
  providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
@@ -753,7 +783,14 @@ export default function MintDashboard() {
753
783
  case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
754
784
  case 'local_openai': return config.localModelName || 'llama3'
755
785
  case 'ollama': return config.ollamaModel || 'llama3:latest'
756
- default: return ''
786
+ default: {
787
+ if (provider.startsWith('custom:')) {
788
+ const id = provider.replace(/^custom:/, '')
789
+ const cp = (config.customProviders ?? []).find((p: any) => p.id === id)
790
+ return (config.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
791
+ }
792
+ return ''
793
+ }
757
794
  }
758
795
  }
759
796
 
@@ -795,6 +832,12 @@ export default function MintDashboard() {
795
832
  config.localModelName = modelName
796
833
  } else if (provider === 'ollama') {
797
834
  config.ollamaModel = modelName
835
+ } else if (provider.startsWith('custom:')) {
836
+ const id = provider.replace(/^custom:/, '')
837
+ config.customModelSelections = {
838
+ ...(config.customModelSelections ?? {}),
839
+ [id]: modelName
840
+ }
798
841
  }
799
842
  await window.settingsApi.saveSettings(config)
800
843
  setSettingsConfig(config)
@@ -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)