@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,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 = true
68
+ isDesktopApp = true,
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,6 +921,216 @@ 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 this was the active provider, reset to gemini
951
+ if (config.aiProvider === `custom:${cp.id}`) {
952
+ updateField('aiProvider', 'gemini')
953
+ }
954
+ }
955
+
956
+ const updateModel = (mIdx: number, patch: Partial<CustomProviderModel>) => {
957
+ const updatedModels = cp.models.map((m, i) => i === mIdx ? { ...m, ...patch } : m)
958
+ updateCp({ models: updatedModels })
959
+ }
960
+ const deleteModel = (mIdx: number) => {
961
+ updateCp({ models: cp.models.filter((_, i) => i !== mIdx) })
962
+ }
963
+ const addModel = () => {
964
+ updateCp({ models: [...cp.models, { modelId: '', displayName: '' }] })
965
+ }
966
+
967
+ const updateHeader = (hIdx: number, patch: Partial<CustomProviderHeader>) => {
968
+ const updatedHeaders = cp.headers.map((h, i) => i === hIdx ? { ...h, ...patch } : h)
969
+ updateCp({ headers: updatedHeaders })
970
+ }
971
+ const deleteHeader = (hIdx: number) => {
972
+ updateCp({ headers: cp.headers.filter((_, i) => i !== hIdx) })
973
+ }
974
+ const addHeader = () => {
975
+ updateCp({ headers: [...cp.headers, { name: '', value: '' }] })
976
+ }
977
+
978
+ return (
979
+ <div key={cpIdx} className={`provider-card ${isActive ? 'active-provider' : ''}`}>
980
+ <div className="provider-card-header">
981
+ <div className="provider-card-title">
982
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
983
+ <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" />
984
+ </svg>
985
+ {cp.displayName || cp.id || 'Unnamed Provider'}
986
+ </div>
987
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
988
+ {isActive && <span className="provider-active-badge">Active</span>}
989
+ <button
990
+ className="btn btn-primary"
991
+ style={{ padding: '3px 8px', fontSize: '12px', background: savedProviderId === cp.id ? '#10b981' : '' }}
992
+ onClick={() => handleSaveProvider(cp.id)}
993
+ title="Save this provider settings"
994
+ >
995
+ {savedProviderId === cp.id ? 'Saved! ✓' : 'Save'}
996
+ </button>
997
+ <button
998
+ className="btn btn-danger"
999
+ style={{ padding: '3px 8px', fontSize: '12px' }}
1000
+ onClick={deleteCp}
1001
+ title="Delete this provider"
1002
+ >
1003
+ Delete
1004
+ </button>
1005
+ </div>
1006
+ </div>
1007
+
1008
+ <div className="provider-card-body">
1009
+ <div className="setting-row">
1010
+ <label>Name</label>
1011
+ <input
1012
+ type="text"
1013
+ value={cp.displayName}
1014
+ onChange={(e) => {
1015
+ const name = e.target.value
1016
+ const computedId = name
1017
+ .toLowerCase()
1018
+ .replace(/[^a-z0-9_-]/g, '-')
1019
+ .replace(/-+/g, '-')
1020
+ .replace(/^-|-$/g, '') || `provider-${cpIdx + 1}`
1021
+ updateCp({ displayName: name, id: computedId })
1022
+ }}
1023
+ placeholder="e.g. DeepSeek"
1024
+ />
1025
+ </div>
1026
+
1027
+ {/* Endpoint */}
1028
+ <div className="setting-row">
1029
+ <label>Base URL</label>
1030
+ <input
1031
+ type="text"
1032
+ value={cp.baseUrl}
1033
+ onChange={(e) => updateCp({ baseUrl: e.target.value })}
1034
+ placeholder="https://api.myprovider.com/v1"
1035
+ />
1036
+ </div>
1037
+
1038
+ <div className="setting-row">
1039
+ <label>API Key</label>
1040
+ <input
1041
+ type="password"
1042
+ value={cp.apiKey}
1043
+ onChange={(e) => updateCp({ apiKey: e.target.value })}
1044
+ placeholder="Optional. Leave empty if you manage auth via headers."
1045
+ />
1046
+ </div>
1047
+
1048
+ {/* Models */}
1049
+ <div style={{ marginTop: '12px' }}>
1050
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Model list</label>
1051
+ {cp.models.map((m, mIdx) => (
1052
+ <div key={mIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1053
+ <input
1054
+ type="text"
1055
+ value={m.modelId}
1056
+ onChange={(e) => updateModel(mIdx, { modelId: e.target.value, displayName: e.target.value })}
1057
+ placeholder="e.g. llama-3.1-8b"
1058
+ style={{ flex: 1 }}
1059
+ />
1060
+ <button
1061
+ className="btn btn-danger"
1062
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1063
+ onClick={() => deleteModel(mIdx)}
1064
+ title="Remove model"
1065
+ >🗑</button>
1066
+ </div>
1067
+ ))}
1068
+ <button
1069
+ className="btn btn-secondary"
1070
+ style={{ fontSize: '12px', marginTop: '4px' }}
1071
+ onClick={addModel}
1072
+ >
1073
+ + Add model
1074
+ </button>
1075
+ </div>
1076
+
1077
+ {/* Headers */}
1078
+ <div style={{ marginTop: '12px' }}>
1079
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Headers (optional)</label>
1080
+ {cp.headers.map((h, hIdx) => (
1081
+ <div key={hIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1082
+ <input
1083
+ type="text"
1084
+ value={h.name}
1085
+ onChange={(e) => updateHeader(hIdx, { name: e.target.value })}
1086
+ placeholder="Header-Name"
1087
+ style={{ flex: 1 }}
1088
+ />
1089
+ <input
1090
+ type="text"
1091
+ value={h.value}
1092
+ onChange={(e) => updateHeader(hIdx, { value: e.target.value })}
1093
+ placeholder="value"
1094
+ style={{ flex: 1 }}
1095
+ />
1096
+ <button
1097
+ className="btn btn-danger"
1098
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1099
+ onClick={() => deleteHeader(hIdx)}
1100
+ title="Remove header"
1101
+ >🗑</button>
1102
+ </div>
1103
+ ))}
1104
+ <button
1105
+ className="btn btn-secondary"
1106
+ style={{ fontSize: '12px', marginTop: '4px' }}
1107
+ onClick={addHeader}
1108
+ >
1109
+ + Add header
1110
+ </button>
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+ )
1115
+ })}
1116
+ </div>
1117
+
1118
+ <div className="setting-actions" style={{ marginTop: '16px' }}>
1119
+ <button
1120
+ className="btn btn-secondary"
1121
+ onClick={() => {
1122
+ const newId = `provider-${(config.customProviders ?? []).length + 1}`
1123
+ updateField('customProviders', [
1124
+ ...(config.customProviders ?? []),
1125
+ { id: newId, displayName: '', baseUrl: '', apiKey: '', models: [], headers: [] }
1126
+ ])
1127
+ }}
1128
+ >
1129
+ + Add custom provider
1130
+ </button>
1131
+ </div>
1132
+ </section>
1133
+
880
1134
 
881
1135
  {isDesktopApp && (
882
1136
  <section className="setting-section">
@@ -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'
@@ -401,6 +426,47 @@ export default function SettingsWindow() {
401
426
  }
402
427
  }
403
428
 
429
+ const handleSaveWithoutClosing = async () => {
430
+ const finalConfig = { ...config }
431
+
432
+ if (config.geminiModel === 'custom') {
433
+ finalConfig.geminiModel = customGemini || 'gemini-2.5-flash'
434
+ }
435
+ if (config.openaiModel === 'custom') {
436
+ finalConfig.openaiModel = customOpenAI || 'gpt-4o'
437
+ }
438
+ if (config.openrouterModel === 'custom') {
439
+ finalConfig.openrouterModel = customOpenRouter || 'openai/gpt-4o-mini'
440
+ }
441
+ if (config.deepseekModel === 'custom') {
442
+ finalConfig.deepseekModel = customDeepSeek || 'deepseek-v4-flash'
443
+ }
444
+ if (config.anthropicModel === 'custom') {
445
+ finalConfig.anthropicModel = customAnthropic || 'claude-3-5-sonnet-latest'
446
+ }
447
+ if (config.hfModel === 'custom') {
448
+ finalConfig.hfModel = customHF || 'meta-llama/Meta-Llama-3-8B-Instruct'
449
+ }
450
+ if (config.localModelName === 'custom') {
451
+ finalConfig.localModelName = customLocal || 'local-model'
452
+ }
453
+ if (config.ollamaModel === 'custom') {
454
+ finalConfig.ollamaModel = customOllama || 'llama3:latest'
455
+ }
456
+
457
+ try {
458
+ await setProfileValue('name', userName)
459
+ await setProfileValue('preferences', userPreferences)
460
+ } catch (e) {
461
+ console.error("Failed to save user profile memory:", e)
462
+ }
463
+
464
+ if (window.settingsApi) {
465
+ await window.settingsApi.saveSettings(finalConfig)
466
+ applyThemeStyles(finalConfig)
467
+ }
468
+ }
469
+
404
470
  const handleReset = async () => {
405
471
  if (confirm('Reset all settings to default?')) {
406
472
  setConfig(DEFAULT_CONFIG)
@@ -656,6 +722,7 @@ export default function SettingsWindow() {
656
722
  updateMessage={updateMessage}
657
723
  handleCheckUpdates={handleCheckUpdates}
658
724
  handleInstallUpdate={handleInstallUpdate}
725
+ onSaveWithoutClosing={handleSaveWithoutClosing}
659
726
  />
660
727
  )}
661
728
 
@@ -1,60 +1,12 @@
1
1
  import React, { useEffect, useState } from 'react'
2
2
  import { getWorkspaceTree, type WorkspaceTreeEntry, createWorkspaceFile, createWorkspaceFolder, deleteWorkspaceItem } from '../tauri'
3
- import folderIcon from 'material-icon-theme/icons/folder.svg?url'
4
- import folderOpenIcon from 'material-icon-theme/icons/folder-open.svg?url'
5
- import folderComponentsIcon from 'material-icon-theme/icons/folder-components.svg?url'
6
- import folderComponentsOpenIcon from 'material-icon-theme/icons/folder-components-open.svg?url'
7
- import folderConfigIcon from 'material-icon-theme/icons/folder-config.svg?url'
8
- import folderConfigOpenIcon from 'material-icon-theme/icons/folder-config-open.svg?url'
9
- import folderDistIcon from 'material-icon-theme/icons/folder-dist.svg?url'
10
- import folderDistOpenIcon from 'material-icon-theme/icons/folder-dist-open.svg?url'
11
- import folderDocsIcon from 'material-icon-theme/icons/folder-docs.svg?url'
12
- import folderDocsOpenIcon from 'material-icon-theme/icons/folder-docs-open.svg?url'
13
- import folderGitIcon from 'material-icon-theme/icons/folder-git.svg?url'
14
- import folderGitOpenIcon from 'material-icon-theme/icons/folder-git-open.svg?url'
15
- import folderGithubIcon from 'material-icon-theme/icons/folder-github.svg?url'
16
- import folderGithubOpenIcon from 'material-icon-theme/icons/folder-github-open.svg?url'
17
- import folderImagesIcon from 'material-icon-theme/icons/folder-images.svg?url'
18
- import folderImagesOpenIcon from 'material-icon-theme/icons/folder-images-open.svg?url'
19
- import folderLogIcon from 'material-icon-theme/icons/folder-log.svg?url'
20
- import folderLogOpenIcon from 'material-icon-theme/icons/folder-log-open.svg?url'
21
- import folderNodeIcon from 'material-icon-theme/icons/folder-node.svg?url'
22
- import folderNodeOpenIcon from 'material-icon-theme/icons/folder-node-open.svg?url'
23
- import folderPublicIcon from 'material-icon-theme/icons/folder-public.svg?url'
24
- import folderPublicOpenIcon from 'material-icon-theme/icons/folder-public-open.svg?url'
25
- import folderRustIcon from 'material-icon-theme/icons/folder-rust.svg?url'
26
- import folderRustOpenIcon from 'material-icon-theme/icons/folder-rust-open.svg?url'
27
- import folderScriptsIcon from 'material-icon-theme/icons/folder-scripts.svg?url'
28
- import folderScriptsOpenIcon from 'material-icon-theme/icons/folder-scripts-open.svg?url'
29
- import folderSrcIcon from 'material-icon-theme/icons/folder-src.svg?url'
30
- import folderSrcOpenIcon from 'material-icon-theme/icons/folder-src-open.svg?url'
31
- import folderSrcTauriIcon from 'material-icon-theme/icons/folder-src-tauri.svg?url'
32
- import folderSrcTauriOpenIcon from 'material-icon-theme/icons/folder-src-tauri-open.svg?url'
33
- import folderTestIcon from 'material-icon-theme/icons/folder-test.svg?url'
34
- import folderTestOpenIcon from 'material-icon-theme/icons/folder-test-open.svg?url'
35
- import folderUiIcon from 'material-icon-theme/icons/folder-ui.svg?url'
36
- import folderUiOpenIcon from 'material-icon-theme/icons/folder-ui-open.svg?url'
37
- import folderUtilsIcon from 'material-icon-theme/icons/folder-utils.svg?url'
38
- import folderUtilsOpenIcon from 'material-icon-theme/icons/folder-utils-open.svg?url'
39
- import cssIcon from 'material-icon-theme/icons/css.svg?url'
40
- import documentIcon from 'material-icon-theme/icons/document.svg?url'
41
- import htmlIcon from 'material-icon-theme/icons/html.svg?url'
42
- import imageIcon from 'material-icon-theme/icons/image.svg?url'
43
- import javascriptIcon from 'material-icon-theme/icons/javascript.svg?url'
44
- import jsonIcon from 'material-icon-theme/icons/json.svg?url'
45
- import lockIcon from 'material-icon-theme/icons/lock.svg?url'
46
- import logIcon from 'material-icon-theme/icons/log.svg?url'
47
- import markdownIcon from 'material-icon-theme/icons/markdown.svg?url'
48
- import npmIcon from 'material-icon-theme/icons/npm.svg?url'
49
- import reactIcon from 'material-icon-theme/icons/react.svg?url'
50
- import reactTsIcon from 'material-icon-theme/icons/react_ts.svg?url'
51
- import rustIcon from 'material-icon-theme/icons/rust.svg?url'
52
- import settingsIcon from 'material-icon-theme/icons/settings.svg?url'
53
- import tauriIcon from 'material-icon-theme/icons/tauri.svg?url'
54
- import tomlIcon from 'material-icon-theme/icons/toml.svg?url'
55
- import typescriptIcon from 'material-icon-theme/icons/typescript.svg?url'
56
- import viteIcon from 'material-icon-theme/icons/vite.svg?url'
57
- import yamlIcon from 'material-icon-theme/icons/yaml.svg?url'
3
+ import {
4
+ materialFolderIcon,
5
+ materialFileIcon,
6
+ getExtension as extension,
7
+ folderOpenIcon
8
+ } from '../../shared/utils/fileIcons'
9
+
58
10
 
59
11
  interface WorkspacePanelProps {
60
12
  agentMode: boolean
@@ -76,76 +28,6 @@ const FILE_LABEL: Record<string, string> = {
76
28
  tsx: 'TS',
77
29
  }
78
30
 
79
- const FOLDER_ICONS: Record<string, { closed: string; open: string }> = {
80
- '.cargo_home': { closed: folderRustIcon, open: folderRustOpenIcon },
81
- '.github': { closed: folderGithubIcon, open: folderGithubOpenIcon },
82
- '.git': { closed: folderGitIcon, open: folderGitOpenIcon },
83
- '.rustup': { closed: folderRustIcon, open: folderRustOpenIcon },
84
- '.rustup_copy': { closed: folderRustIcon, open: folderRustOpenIcon },
85
- '.rustup_home': { closed: folderRustIcon, open: folderRustOpenIcon },
86
- assets: { closed: folderImagesIcon, open: folderImagesOpenIcon },
87
- components: { closed: folderComponentsIcon, open: folderComponentsOpenIcon },
88
- crates: { closed: folderRustIcon, open: folderRustOpenIcon },
89
- css: { closed: folderUiIcon, open: folderUiOpenIcon },
90
- dist: { closed: folderDistIcon, open: folderDistOpenIcon },
91
- docs: { closed: folderDocsIcon, open: folderDocsOpenIcon },
92
- logs: { closed: folderLogIcon, open: folderLogOpenIcon },
93
- node_modules: { closed: folderNodeIcon, open: folderNodeOpenIcon },
94
- out: { closed: folderDistIcon, open: folderDistOpenIcon },
95
- public: { closed: folderPublicIcon, open: folderPublicOpenIcon },
96
- renderer: { closed: folderUiIcon, open: folderUiOpenIcon },
97
- scripts: { closed: folderScriptsIcon, open: folderScriptsOpenIcon },
98
- src: { closed: folderSrcIcon, open: folderSrcOpenIcon },
99
- 'src-tauri': { closed: folderSrcTauriIcon, open: folderSrcTauriOpenIcon },
100
- tests: { closed: folderTestIcon, open: folderTestOpenIcon },
101
- utils: { closed: folderUtilsIcon, open: folderUtilsOpenIcon },
102
- }
103
-
104
- const FILE_ICONS_BY_EXTENSION: Record<string, string> = {
105
- css: cssIcon,
106
- html: htmlIcon,
107
- jpeg: imageIcon,
108
- jpg: imageIcon,
109
- js: javascriptIcon,
110
- json: jsonIcon,
111
- lock: lockIcon,
112
- log: logIcon,
113
- md: markdownIcon,
114
- png: imageIcon,
115
- rs: rustIcon,
116
- svg: imageIcon,
117
- toml: tomlIcon,
118
- ts: typescriptIcon,
119
- tsx: reactTsIcon,
120
- yaml: yamlIcon,
121
- yml: yamlIcon,
122
- }
123
-
124
- const FILE_ICONS_BY_NAME: Record<string, string> = {
125
- 'package-lock.json': npmIcon,
126
- 'package.json': npmIcon,
127
- 'tauri.conf.json': tauriIcon,
128
- 'vite.config.ts': viteIcon,
129
- 'vite.config.web.ts': viteIcon,
130
- }
131
-
132
- function extension(name: string) {
133
- const index = name.lastIndexOf('.')
134
- return index === -1 ? '' : name.slice(index + 1).toLowerCase()
135
- }
136
-
137
- function materialFolderIcon(name: string, open: boolean) {
138
- const icon = FOLDER_ICONS[name] || FOLDER_ICONS[name.toLowerCase()]
139
- if (!icon) return open ? folderOpenIcon : folderIcon
140
- return open ? icon.open : icon.closed
141
- }
142
-
143
- function materialFileIcon(name: string, fileExtension: string) {
144
- if (name.endsWith('.config.ts') || name.endsWith('.config.js')) return settingsIcon
145
- if (name.endsWith('.tsx')) return reactTsIcon
146
- if (name.endsWith('.jsx')) return reactIcon
147
- return FILE_ICONS_BY_NAME[name.toLowerCase()] || FILE_ICONS_BY_EXTENSION[fileExtension] || documentIcon
148
- }
149
31
 
150
32
  function TreeNode({ entry, level, onRefresh, workspacePath }: { entry: WorkspaceTreeEntry; level: number; onRefresh: () => void; workspacePath: string; key?: string }) {
151
33
  const [open, setOpen] = useState(level < 1)
@@ -1670,12 +1670,28 @@ h1 {
1670
1670
  width: 15px;
1671
1671
  }
1672
1672
 
1673
+ .agent-activity-icon[data-has-img="true"]::before,
1674
+ .agent-activity-icon[data-has-img="true"]::after {
1675
+ display: none !important;
1676
+ }
1677
+
1678
+ .agent-activity-icon img {
1679
+ width: 14px;
1680
+ height: 14px;
1681
+ display: block;
1682
+ object-fit: contain;
1683
+ filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.12));
1684
+ pointer-events: none;
1685
+ user-select: none;
1686
+ }
1687
+
1673
1688
  .agent-activity-icon::before,
1674
1689
  .agent-activity-icon::after {
1675
1690
  content: "";
1676
1691
  position: absolute;
1677
1692
  }
1678
1693
 
1694
+
1679
1695
  .agent-activity-item[data-kind="folder"] .agent-activity-icon::before {
1680
1696
  background: var(--text-muted);
1681
1697
  border-radius: 2px 2px 1px 1px;
@@ -172,8 +172,13 @@ export default function ChatPanel({
172
172
  case 'ollama':
173
173
  return dynamicOllamaModels
174
174
  default:
175
+ if (provider.startsWith('custom:')) {
176
+ const id = provider.replace(/^custom:/, '')
177
+ const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
178
+ return cp?.models.map(m => m.modelId) ?? []
179
+ }
175
180
  return []
176
- }
181
+ }
177
182
  }
178
183
 
179
184
  const activeProvider = status?.activeProvider ?? ''
@@ -199,6 +204,11 @@ export default function ChatPanel({
199
204
  case 'ollama':
200
205
  return settingsConfig.ollamaModel
201
206
  default:
207
+ if (provider.startsWith('custom:')) {
208
+ const id = provider.replace(/^custom:/, '')
209
+ const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
210
+ return (settingsConfig.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
211
+ }
202
212
  return ''
203
213
  }
204
214
  }
@@ -370,14 +380,19 @@ export default function ChatPanel({
370
380
  }
371
381
  }
372
382
  if (sending) return
373
- if (!settingsConfig?.enableVoiceReply && !voiceMode) {
374
- lastAutoSpokenIdRef.current = latest?.id ?? null
383
+ if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
384
+
385
+ if (!settingsConfig?.enableVoiceReply) {
386
+ lastAutoSpokenIdRef.current = latest.id
387
+ if (voiceMode) {
388
+ scheduleVoiceListen(350)
389
+ }
375
390
  return
376
391
  }
377
- if (!latest?.aiText || latest.id === lastAutoSpokenIdRef.current) return
392
+
378
393
  lastAutoSpokenIdRef.current = latest.id
379
394
  speak(latest.aiText)
380
- }, [interactions, sending, settingsConfig?.enableVoiceReply])
395
+ }, [interactions, sending, settingsConfig?.enableVoiceReply, voiceMode])
381
396
 
382
397
  // Drag and Drop Zone Overlay
383
398
  const [isDragging, setIsDragging] = useState(false)
@@ -1137,6 +1152,11 @@ export default function ChatPanel({
1137
1152
  else if (provider === 'huggingface') displayName = 'HF'
1138
1153
  else if (provider === 'local_openai') displayName = 'Local'
1139
1154
  else if (provider === 'ollama') displayName = 'Ollama'
1155
+ else if (provider.startsWith('custom:')) {
1156
+ const id = provider.replace(/^custom:/, '')
1157
+ const cp = (settingsConfig?.customProviders ?? []).find(p => p.id === id)
1158
+ displayName = cp?.displayName || id
1159
+ }
1140
1160
  return <option key={provider} value={provider}>{displayName}</option>
1141
1161
  })}
1142
1162
  </select>
@@ -606,6 +606,11 @@ export default function MintDashboard() {
606
606
  else if (provider === 'huggingface') providerName = 'HF'
607
607
  else if (provider === 'local_openai') providerName = 'Local'
608
608
  else if (provider === 'ollama') providerName = 'Ollama'
609
+ else if (provider.startsWith('custom:')) {
610
+ const id = provider.replace(/^custom:/, '')
611
+ const cp = (settingsConfig?.customProviders ?? []).find((p: any) => p.id === id)
612
+ providerName = cp?.displayName || id
613
+ }
609
614
 
610
615
  if (providerName && providerName === provider) {
611
616
  providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
@@ -623,7 +628,14 @@ export default function MintDashboard() {
623
628
  case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
624
629
  case 'local_openai': return config.localModelName || 'llama3'
625
630
  case 'ollama': return config.ollamaModel || 'llama3:latest'
626
- default: return ''
631
+ default: {
632
+ if (provider.startsWith('custom:')) {
633
+ const id = provider.replace(/^custom:/, '')
634
+ const cp = (config.customProviders ?? []).find((p: any) => p.id === id)
635
+ return (config.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
636
+ }
637
+ return ''
638
+ }
627
639
  }
628
640
  }
629
641
 
@@ -665,6 +677,12 @@ export default function MintDashboard() {
665
677
  config.localModelName = modelName
666
678
  } else if (provider === 'ollama') {
667
679
  config.ollamaModel = modelName
680
+ } else if (provider.startsWith('custom:')) {
681
+ const id = provider.replace(/^custom:/, '')
682
+ config.customModelSelections = {
683
+ ...(config.customModelSelections ?? {}),
684
+ [id]: modelName
685
+ }
668
686
  }
669
687
  await window.settingsApi.saveSettings(config)
670
688
  setSettingsConfig(config)