@pheem49/mint 1.8.2 → 1.10.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 (106) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +297 -107
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +382 -127
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/image.rs +8 -8
  10. package/crates/mint-cli/src/interactive.rs +2888 -0
  11. package/crates/mint-cli/src/main.rs +391 -2506
  12. package/crates/mint-cli/src/markdown.rs +294 -0
  13. package/crates/mint-cli/src/onboard.rs +152 -109
  14. package/crates/mint-cli/src/setup.rs +143 -143
  15. package/crates/mint-cli/src/updater.rs +48 -6
  16. package/crates/mint-core/Cargo.toml +1 -0
  17. package/crates/mint-core/src/agent_loop.rs +1 -0
  18. package/crates/mint-core/src/api_server.rs +431 -65
  19. package/crates/mint-core/src/browser.rs +630 -102
  20. package/crates/mint-core/src/channels.rs +105 -6
  21. package/crates/mint-core/src/chat.rs +174 -11
  22. package/crates/mint-core/src/code_tools.rs +47 -31
  23. package/crates/mint-core/src/config.rs +302 -17
  24. package/crates/mint-core/src/image_gen.rs +299 -38
  25. package/crates/mint-core/src/lib.rs +10 -7
  26. package/crates/mint-core/src/mcp.rs +119 -6
  27. package/crates/mint-core/src/memory.rs +6 -1
  28. package/crates/mint-core/src/orchestration.rs +237 -96
  29. package/crates/mint-core/src/pictures.rs +67 -5
  30. package/crates/mint-core/src/semantic.rs +1 -1
  31. package/crates/mint-core/src/shell.rs +1 -1
  32. package/crates/mint-core/src/skills.rs +128 -25
  33. package/crates/mint-core/src/video_gen.rs +235 -0
  34. package/package.json +8 -2
  35. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  36. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  37. package/shared-modules/linux-audio/lilv.json +26 -19
  38. package/shared-modules/linux-audio/lv2.json +0 -1
  39. package/shared-modules/luajit/luajit.json +2 -2
  40. package/skills-lock.json +41 -0
  41. package/src/renderer/index-web.html +1 -1
  42. package/src/renderer/index.html +1 -1
  43. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  44. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  45. package/src/renderer/shared/platform.ts +3 -0
  46. package/src/renderer/shared/types.ts +4 -0
  47. package/src/renderer/shared/utils/approval.tsx +86 -7
  48. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  49. package/src/renderer/shared/utils/markdown.tsx +217 -88
  50. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  51. package/src/renderer/src/components/ChatPanel.tsx +486 -69
  52. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  53. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  54. package/src/renderer/src/components/MintDashboard.tsx +195 -7
  55. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  56. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  57. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  58. package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
  59. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  60. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  61. package/src/renderer/src/components/SettingsWindow.tsx +106 -3
  62. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  63. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  64. package/src/renderer/src/css/image-studio.css +106 -0
  65. package/src/renderer/src/css/settings.css +158 -0
  66. package/src/renderer/src/css/styles.css +368 -1
  67. package/src/renderer/src/css/veo-studio.css +626 -0
  68. package/src/renderer/src/index.css +1 -0
  69. package/src/renderer/src/tauri.ts +144 -17
  70. package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
  71. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  72. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  73. package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
  74. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  75. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  76. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  77. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
  78. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  79. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  80. package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
  81. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  82. package/src/renderer/src-web/css/image-studio.css +106 -0
  83. package/src/renderer/src-web/css/settings.css +158 -0
  84. package/src/renderer/src-web/css/styles.css +369 -1
  85. package/src/renderer/src-web/css/veo-studio.css +650 -0
  86. package/src/renderer/src-web/index.css +1 -0
  87. package/src/renderer/src-web/tauri.ts +135 -16
  88. package/src-tauri/src/desktop.rs +18 -16
  89. package/src-tauri/src/discord_rpc.rs +1 -1
  90. package/src-tauri/src/lib.rs +155 -25
  91. package/src-tauri/src/webhooks.rs +45 -31
  92. package/src-tauri/src/workflows.rs +1 -0
  93. package/src-tauri/tauri.conf.json +1 -1
  94. package/temp_gridgeist/AGENTS.md +8 -0
  95. package/temp_gridgeist/CHANGELOG.md +130 -0
  96. package/temp_gridgeist/LICENSE +21 -0
  97. package/temp_gridgeist/README.md +323 -0
  98. package/temp_gridgeist/README.th.md +325 -0
  99. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  100. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  101. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  102. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  103. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  104. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  105. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  106. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -13,11 +13,126 @@ interface PluginsTabProps {
13
13
  setMcpArgs: (val: string) => void
14
14
  mcpEnv: string
15
15
  setMcpEnv: (val: string) => void
16
+ mcpIcon?: string
17
+ setMcpIcon?: (val: string) => void
16
18
  handleAddMcpServer: () => void
17
19
  handleRemoveMcpServer: (name: string) => void
18
20
  handleConnectPlugin: (plugin: string) => void
19
21
  }
20
22
 
23
+ function renderMcpSvgIcon(name: string, customSvgOrUrl?: string) {
24
+ if (customSvgOrUrl && customSvgOrUrl.trim()) {
25
+ const trimmed = customSvgOrUrl.trim()
26
+ if (trimmed.startsWith('<svg') || trimmed.includes('<svg')) {
27
+ return (
28
+ <span
29
+ style={{ width: '22px', height: '22px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
30
+ dangerouslySetInnerHTML={{ __html: trimmed }}
31
+ />
32
+ )
33
+ }
34
+ if (trimmed.startsWith('data:image') || trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
35
+ return <img src={trimmed} alt={name} style={{ width: '22px', height: '22px', objectFit: 'contain' }} />
36
+ }
37
+ const cleanCustom = trimmed.toLowerCase()
38
+ if (cleanCustom === 'search') {
39
+ return (
40
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
41
+ <circle cx="11" cy="11" r="8" />
42
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
43
+ </svg>
44
+ )
45
+ }
46
+ if (cleanCustom === 'database') {
47
+ return (
48
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#a855f7" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
49
+ <ellipse cx="12" cy="5" rx="9" ry="3"/>
50
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
51
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
52
+ </svg>
53
+ )
54
+ }
55
+ if (cleanCustom === 'cloud') {
56
+ return (
57
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#38bdf8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
58
+ <path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
59
+ </svg>
60
+ )
61
+ }
62
+ if (cleanCustom === 'code' || cleanCustom === 'terminal') {
63
+ return (
64
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
65
+ <polyline points="16 18 22 12 16 6" />
66
+ <polyline points="8 6 2 12 8 18" />
67
+ </svg>
68
+ )
69
+ }
70
+ if (cleanCustom === 'api' || cleanCustom === 'bolt') {
71
+ return (
72
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
73
+ <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
74
+ </svg>
75
+ )
76
+ }
77
+ }
78
+
79
+ const cleanName = name.toLowerCase().trim()
80
+ if (cleanName === 'docker') {
81
+ return (
82
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="#0db7ed">
83
+ <path d="M13 8.5h2v2h-2zm-3 0h2v2h-2zm-3 0h2v2H7zm-3 3h2v2H4zm3 0h2v2H7zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-12 3h2v2H4zm3 0h2v2H7zm3 0h2v2h-2zm3 0h2v2h-2zm3 0h2v2h-2zm-6.2 3.8c-.8.5-2.1.8-3.4.6-2.5-.3-4.5-2.2-4.8-4.7h-1c.4 3.4 3.1 6 6.5 6 3 0 5.6-2 6.4-4.8.6-.2 1.4-.4 2.2-.2.3.1.6.3.8.5.6.6 1.4.9 2.2.9h.4c.5-.7.8-1.5.8-2.4 0-.3 0-.6-.1-.9-.7.1-1.4 0-2.1-.3-.6-.3-1.1-.8-1.5-1.4l-.2-.3h-2.1c-.2.7-.6 1.3-1.2 1.7-.6.4-1.3.6-2 .4z"/>
84
+ </svg>
85
+ )
86
+ }
87
+ if (cleanName === 'git' || cleanName === 'gitkraken') {
88
+ return (
89
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#f05032" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
90
+ <circle cx="18" cy="18" r="3" />
91
+ <circle cx="6" cy="6" r="3" />
92
+ <circle cx="6" cy="18" r="3" />
93
+ <path d="M6 9v6" />
94
+ <path d="M9 18h6" />
95
+ </svg>
96
+ )
97
+ }
98
+ if (cleanName === 'github') {
99
+ return (
100
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="#f0f6fc">
101
+ <path fillRule="evenodd" clipRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.53 1.032 1.53 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
102
+ </svg>
103
+ )
104
+ }
105
+ if (cleanName === 'node' || cleanName === 'nodejs') {
106
+ return (
107
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="#68a063">
108
+ <path d="M12 2L2 7.5v9L12 22l10-5.5v-9L12 2zm0 2.3l7.7 4.2v7L12 19.7 4.3 15.5v-7L12 4.3zm-1 4.2v3.5l-3-1.7V8.5l3 1.7zm2 0l3-1.7v1.8l-3 1.7V8.5z"/>
109
+ </svg>
110
+ )
111
+ }
112
+ if (cleanName === 'spotify') {
113
+ return (
114
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="#1ed760">
115
+ <path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm4.586 14.424c-.18.295-.563.387-.857.207-2.35-1.434-5.308-1.758-8.793-.963-.335.077-.67-.133-.746-.467-.077-.334.132-.67.467-.746 3.812-.871 7.102-.494 9.722 1.112.294.18.386.563.207.857zm1.233-2.743c-.226.367-.706.482-1.073.257-2.687-1.652-6.785-2.131-9.965-1.166-.413.126-.847-.106-.973-.519-.125-.413.106-.847.519-.973 3.632-1.102 8.147-.568 11.235 1.328.367.226.482.706.257 1.073zm.105-2.835C14.692 8.95 9.375 8.775 6.297 9.71c-.496.15-1.022-.132-1.173-.628-.15-.496.132-1.022.628-1.173 3.535-1.073 9.404-.871 12.984 1.254.446.265.592.844.327 1.29-.265.446-.844.592-1.29.327z"/>
116
+ </svg>
117
+ )
118
+ }
119
+ if (cleanName === 'discord') {
120
+ return (
121
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="#5865f2">
122
+ <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994.021-.041.001-.09-.041-.106a13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.893.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
123
+ </svg>
124
+ )
125
+ }
126
+ return (
127
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
128
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
129
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
130
+ <line x1="6" y1="6" x2="6.01" y2="6"/>
131
+ <line x1="6" y1="18" x2="6.01" y2="18"/>
132
+ </svg>
133
+ )
134
+ }
135
+
21
136
  export default function PluginsTab({
22
137
  config,
23
138
  updateField,
@@ -29,6 +144,8 @@ export default function PluginsTab({
29
144
  setMcpArgs,
30
145
  mcpEnv,
31
146
  setMcpEnv,
147
+ mcpIcon = '',
148
+ setMcpIcon,
32
149
  handleAddMcpServer,
33
150
  handleRemoveMcpServer,
34
151
  handleConnectPlugin
@@ -123,6 +240,20 @@ export default function PluginsTab({
123
240
  }
124
241
  }
125
242
 
243
+ const [expandedMcp, setExpandedMcp] = useState<string | null>(null)
244
+
245
+ const handleUpdateMcpServerField = (serverName: string, field: string, value: any) => {
246
+ const currentMcp = config.mcpServers?.[serverName] || { command: 'npx', args: [], env: {} }
247
+ const updatedMcp = {
248
+ ...(config.mcpServers || {}),
249
+ [serverName]: {
250
+ ...currentMcp,
251
+ [field]: value
252
+ }
253
+ }
254
+ updateField('mcpServers', updatedMcp)
255
+ }
256
+
126
257
  const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null)
127
258
 
128
259
  const toggleExpand = (pluginKey: string) => {
@@ -161,6 +292,7 @@ export default function PluginsTab({
161
292
  command: string
162
293
  args: string[]
163
294
  icon: string
295
+ customIcon?: string
164
296
  isEnabled: boolean
165
297
  isConfigured: boolean
166
298
  description: string
@@ -178,6 +310,7 @@ export default function PluginsTab({
178
310
  command: srv.command,
179
311
  args: srv.args || [],
180
312
  icon,
313
+ customIcon: srv.icon,
181
314
  isEnabled: true,
182
315
  isConfigured: true,
183
316
  description: `Command: ${srv.command} ${(srv.args || []).join(' ')}`
@@ -374,80 +507,171 @@ export default function PluginsTab({
374
507
  No MCP servers configured or discovered.
375
508
  </div>
376
509
  ) : (
377
- mcpListItems.map((item) => (
378
- <div
379
- className="plugin-card"
380
- key={item.name}
381
- style={{
382
- display: 'flex',
383
- alignItems: 'center',
384
- justifyContent: 'space-between',
385
- padding: '16px 20px',
386
- border: '1px solid var(--border)',
387
- borderRadius: '12px',
388
- background: item.isConfigured ? 'var(--surface-bg)' : 'rgba(16, 185, 129, 0.03)',
389
- opacity: item.isConfigured ? 1 : 0.65,
390
- marginBottom: '10px',
391
- transition: 'all 0.2s'
392
- }}
393
- >
394
- <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
395
- <div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>
396
- {item.icon}
397
- </div>
398
- <div className="plugin-info">
399
- <div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>
400
- {item.name} {!item.isConfigured && <span style={{ fontSize: '0.75rem', padding: '2px 6px', background: 'rgba(16, 185, 129, 0.15)', color: '#10b981', borderRadius: '4px', marginLeft: '6px' }}>Discovered</span>}
510
+ mcpListItems.map((item) => {
511
+ const isExpanded = expandedMcp === item.name;
512
+ const srvConfig = config.mcpServers?.[item.name] || { command: item.command, args: item.args, env: {}, icon: item.customIcon };
513
+
514
+ return (
515
+ <div
516
+ className={`plugin-card-wrapper ${isExpanded ? 'active-plugin-card' : ''}`}
517
+ key={item.name}
518
+ style={{
519
+ border: '1px solid var(--border)',
520
+ borderRadius: '12px',
521
+ background: item.isConfigured ? 'var(--surface-bg)' : 'rgba(16, 185, 129, 0.03)',
522
+ opacity: item.isConfigured ? 1 : 0.65,
523
+ marginBottom: '10px',
524
+ transition: 'all 0.2s ease',
525
+ overflow: 'hidden'
526
+ }}
527
+ >
528
+ <div className="plugin-card" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px' }}>
529
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
530
+ <div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>
531
+ {renderMcpSvgIcon(item.name, item.customIcon)}
532
+ </div>
533
+ <div className="plugin-info">
534
+ <div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>
535
+ {item.name} {!item.isConfigured && <span style={{ fontSize: '0.75rem', padding: '2px 6px', background: 'rgba(16, 185, 129, 0.15)', color: '#10b981', borderRadius: '4px', marginLeft: '6px' }}>Discovered</span>}
536
+ </div>
537
+ <div className="plugin-desc" style={{ fontFamily: 'monospace', fontSize: '0.8rem', color: 'var(--text-muted)' }}>
538
+ {item.description}
539
+ </div>
540
+ </div>
401
541
  </div>
402
- <div className="plugin-desc" style={{ fontFamily: 'monospace', fontSize: '0.8rem', color: 'var(--text-muted)' }}>
403
- {item.description}
542
+ <div className="plugin-actions" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
543
+ {item.isConfigured && (
544
+ <>
545
+ <button
546
+ className="btn-icon-edit"
547
+ onClick={() => setExpandedMcp(isExpanded ? null : item.name)}
548
+ style={{
549
+ background: isExpanded ? 'var(--accent)' : 'transparent',
550
+ border: 'none',
551
+ color: isExpanded ? '#fff' : 'var(--text-soft)',
552
+ cursor: 'pointer',
553
+ padding: '6px',
554
+ display: 'flex',
555
+ alignItems: 'center',
556
+ justifyContent: 'center',
557
+ borderRadius: '6px',
558
+ transition: 'all 0.2s'
559
+ }}
560
+ title="Edit MCP Server Settings"
561
+ >
562
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
563
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
564
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
565
+ </svg>
566
+ </button>
567
+ <button
568
+ className="btn-icon-danger"
569
+ onClick={() => handleRemoveMcpServer(item.name)}
570
+ style={{
571
+ background: 'transparent',
572
+ border: 'none',
573
+ color: '#ef4444',
574
+ cursor: 'pointer',
575
+ padding: '6px',
576
+ display: 'flex',
577
+ alignItems: 'center',
578
+ justifyContent: 'center',
579
+ borderRadius: '6px',
580
+ transition: 'background 0.2s'
581
+ }}
582
+ title="Delete MCP Server"
583
+ >
584
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
585
+ <polyline points="3 6 5 6 21 6"></polyline>
586
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
587
+ <line x1="10" y1="11" x2="10" y2="17"></line>
588
+ <line x1="14" y1="11" x2="14" y2="17"></line>
589
+ </svg>
590
+ </button>
591
+ </>
592
+ )}
593
+ <label className="settings-toggle-switch">
594
+ <input
595
+ type="checkbox"
596
+ checked={item.isEnabled}
597
+ onChange={(e) => {
598
+ if (e.target.checked) {
599
+ handleEnableTool(item.name, item.command, item.args);
600
+ } else {
601
+ handleRemoveMcpServer(item.name);
602
+ }
603
+ }}
604
+ />
605
+ <span className="settings-toggle-slider" />
606
+ </label>
404
607
  </div>
405
608
  </div>
406
- </div>
407
- <div className="plugin-actions" style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
408
- {item.isConfigured && (
409
- <button
410
- className="btn-icon-danger"
411
- onClick={() => handleRemoveMcpServer(item.name)}
412
- style={{
413
- background: 'transparent',
414
- border: 'none',
415
- color: '#ef4444',
416
- cursor: 'pointer',
417
- padding: '6px',
418
- display: 'flex',
419
- alignItems: 'center',
420
- justifyContent: 'center',
421
- borderRadius: '6px',
422
- transition: 'background 0.2s'
423
- }}
424
- title="Delete MCP Server"
425
- >
426
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
427
- <polyline points="3 6 5 6 21 6"></polyline>
428
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
429
- <line x1="10" y1="11" x2="10" y2="17"></line>
430
- <line x1="14" y1="11" x2="14" y2="17"></line>
431
- </svg>
432
- </button>
609
+
610
+ {item.isConfigured && isExpanded && (
611
+ <div className="plugin-config-panel" style={{
612
+ padding: '16px 20px',
613
+ borderTop: '1px solid var(--border)',
614
+ background: 'rgba(0, 0, 0, 0.18)',
615
+ display: 'flex',
616
+ flexDirection: 'column',
617
+ gap: '12px'
618
+ }}>
619
+ <h4 style={{ fontSize: '0.85rem', fontWeight: '600', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
620
+ Edit Settings for {item.name}
621
+ </h4>
622
+
623
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
624
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
625
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Command</label>
626
+ <input
627
+ type="text"
628
+ value={srvConfig.command || ''}
629
+ onChange={(e) => handleUpdateMcpServerField(item.name, 'command', e.target.value)}
630
+ style={{ padding: '8px 12px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.85rem' }}
631
+ />
632
+ </div>
633
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
634
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Icon (Optional)</label>
635
+ <input
636
+ type="text"
637
+ placeholder="SVG code, URL, or preset: search/database/cloud/code/api"
638
+ value={srvConfig.icon || ''}
639
+ onChange={(e) => handleUpdateMcpServerField(item.name, 'icon', e.target.value)}
640
+ style={{ padding: '8px 12px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.85rem' }}
641
+ />
642
+ </div>
643
+ </div>
644
+
645
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
646
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Arguments (Space separated)</label>
647
+ <input
648
+ type="text"
649
+ value={(srvConfig.args || []).join(' ')}
650
+ onChange={(e) => handleUpdateMcpServerField(item.name, 'args', e.target.value.split(/\s+/).filter(Boolean))}
651
+ style={{ padding: '8px 12px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.85rem', width: '100%' }}
652
+ />
653
+ </div>
654
+
655
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
656
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Environment Variables (JSON format)</label>
657
+ <textarea
658
+ value={typeof srvConfig.env === 'object' ? JSON.stringify(srvConfig.env, null, 2) : (srvConfig.env || '')}
659
+ onChange={(e) => {
660
+ try {
661
+ const parsed = JSON.parse(e.target.value)
662
+ handleUpdateMcpServerField(item.name, 'env', parsed)
663
+ } catch {
664
+ // allow live typing
665
+ }
666
+ }}
667
+ style={{ padding: '8px 12px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.85rem', fontFamily: 'monospace', height: '65px', resize: 'vertical' }}
668
+ />
669
+ </div>
670
+ </div>
433
671
  )}
434
- <label className="settings-toggle-switch">
435
- <input
436
- type="checkbox"
437
- checked={item.isEnabled}
438
- onChange={(e) => {
439
- if (e.target.checked) {
440
- handleEnableTool(item.name, item.command, item.args);
441
- } else {
442
- handleRemoveMcpServer(item.name);
443
- }
444
- }}
445
- />
446
- <span className="settings-toggle-slider" />
447
- </label>
448
672
  </div>
449
- </div>
450
- ))
673
+ )
674
+ })
451
675
  )}
452
676
  </div>
453
677
 
@@ -501,6 +725,16 @@ export default function PluginsTab({
501
725
  style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%' }}
502
726
  />
503
727
  </div>
728
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '12px' }}>
729
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Icon (Optional)</label>
730
+ <input
731
+ type="text"
732
+ placeholder="SVG code, URL, or preset: search/database/cloud/code/api"
733
+ value={mcpIcon}
734
+ onChange={(e) => setMcpIcon && setMcpIcon(e.target.value)}
735
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%' }}
736
+ />
737
+ </div>
504
738
  <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '14px' }}>
505
739
  <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Environment Variables (JSON)</label>
506
740
  <textarea
@@ -545,7 +779,7 @@ export default function PluginsTab({
545
779
  }}>
546
780
  <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
547
781
  <div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>
548
- {p.icon}
782
+ {renderMcpSvgIcon(p.key)}
549
783
  </div>
550
784
  <div className="plugin-info">
551
785
  <div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>{p.name}</div>
@@ -107,9 +107,14 @@ export default function ThemeTab({ config, updateField }: ThemeTabProps) {
107
107
  <label>Font Family</label>
108
108
  <select value={config.fontFamily} onChange={(e) => updateField('fontFamily', e.target.value)}>
109
109
  <option value="'Outfit', sans-serif">Outfit (Default)</option>
110
- <option value="'Mali', cursive">Mali (Cute Thai Font)</option>
111
110
  <option value="'Prompt', sans-serif">Prompt (Modern Thai)</option>
112
111
  <option value="'Sarabun', sans-serif">Sarabun (Formal Thai)</option>
112
+ <option value="'Kanit', sans-serif">Kanit (Trendy Thai)</option>
113
+ <option value="'Mitr', sans-serif">Mitr (Friendly Thai)</option>
114
+ <option value="'Noto Sans Thai', sans-serif">Noto Sans Thai (Clean Thai)</option>
115
+ <option value="'Mali', cursive">Mali (Cute Thai Font)</option>
116
+ <option value="'Inter', sans-serif">Inter (Clean Sans-Serif)</option>
117
+ <option value="'Fira Code', monospace">Fira Code (Developer Code Font)</option>
113
118
  </select>
114
119
  </div>
115
120
  <div className="setting-row">
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react'
2
- import { getLocalApiBase, isTauriRuntime, getProfileValue, setProfileValue } from '../tauri'
2
+ import { getLocalApiBase, isTauriRuntime, getProfileValue, setProfileValue, setActiveModel } from '../tauri'
3
3
  import GeneralTab from './Settings/GeneralTab'
4
4
  import MemoryTab from './Settings/MemoryTab'
5
5
  import AudioTab from './Settings/AudioTab'
@@ -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',
@@ -70,11 +91,17 @@ export const DEFAULT_CONFIG = {
70
91
  lineChannelAccessToken: '',
71
92
  lineChannelSecret: '',
72
93
  enableLineBridge: false,
94
+ lineWebhookHost: '127.0.0.1',
95
+ lineWebhookPort: 3000,
73
96
  whatsappCloudAccessToken: '',
74
97
  whatsappPhoneNumberId: '',
75
98
  whatsappVerifyToken: '',
76
99
  whatsappAppSecret: '',
77
100
  enableWhatsappBridge: false,
101
+ whatsappWebhookHost: '127.0.0.1',
102
+ whatsappWebhookPort: 3001,
103
+ enableBridgeAckNotification: true,
104
+ bridgeAckMessage: '[Mint Agent] Remote command received, processing...',
78
105
  notionApiKey: '',
79
106
  notionDatabaseId: '',
80
107
  gmailClientId: '',
@@ -93,6 +120,9 @@ export const DEFAULT_CONFIG = {
93
120
  stabilityApiKey: '',
94
121
  ideogramApiKey: '',
95
122
  replicateApiKey: '',
123
+ // Video Generation
124
+ videoGenProvider: 'veo' as 'veo',
125
+ veoModel: 'veo-2.0-generate-001',
96
126
  // Multi-Agent Configuration
97
127
  agents: [
98
128
  {
@@ -122,7 +152,11 @@ export const DEFAULT_CONFIG = {
122
152
  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
153
  enabled: true
124
154
  }
125
- ]
155
+ ],
156
+ // Custom Providers
157
+ customProviders: [] as CustomProviderConfig[],
158
+ // Per-provider model selection: { [providerId]: selectedModelId }
159
+ customModelSelections: {} as Record<string, string>,
126
160
  }
127
161
 
128
162
  type TabType = 'sect-general' | 'sect-audio' | 'sect-automation' | 'sect-theme' | 'sect-plugins' | 'sect-shortcuts' | 'sect-memory' | 'sect-agents'
@@ -214,6 +248,7 @@ export default function SettingsWindow() {
214
248
  const [mcpCmd, setMcpCmd] = useState('')
215
249
  const [mcpArgs, setMcpArgs] = useState('')
216
250
  const [mcpEnv, setMcpEnv] = useState('')
251
+ const [mcpIcon, setMcpIcon] = useState('')
217
252
  const [updateMessage, setUpdateMessage] = useState('')
218
253
  const [updateAvailable, setUpdateAvailable] = useState(false)
219
254
 
@@ -396,6 +431,25 @@ export default function SettingsWindow() {
396
431
  console.error("Failed to save user profile memory:", e)
397
432
  }
398
433
 
434
+ const getActiveModelName = (provider: string, cfg: typeof finalConfig) => {
435
+ switch (provider) {
436
+ case 'gemini': return cfg.geminiModel
437
+ case 'openai': return cfg.openaiModel
438
+ case 'openrouter': return cfg.openrouterModel
439
+ case 'deepseek': return cfg.deepseekModel
440
+ case 'anthropic': return cfg.anthropicModel
441
+ case 'huggingface': return cfg.hfModel
442
+ case 'local_openai': return cfg.localModelName
443
+ case 'ollama': return cfg.ollamaModel
444
+ default: return ''
445
+ }
446
+ }
447
+ const oldModel = getActiveModelName(config.aiProvider, config)
448
+ const newModel = getActiveModelName(finalConfig.aiProvider, finalConfig)
449
+ if (config.aiProvider !== finalConfig.aiProvider || oldModel !== newModel) {
450
+ setActiveModel(finalConfig.aiProvider, newModel).catch(() => {})
451
+ }
452
+
399
453
  if (window.settingsApi) {
400
454
  await window.settingsApi.saveSettings(finalConfig)
401
455
  applyThemeStyles(finalConfig)
@@ -410,6 +464,54 @@ export default function SettingsWindow() {
410
464
  }
411
465
  }
412
466
 
467
+ const handleSaveWithoutClosing = async () => {
468
+ const finalConfig = { ...config }
469
+
470
+ if (config.geminiModel === 'custom') {
471
+ finalConfig.geminiModel = customGemini || 'gemini-2.5-flash'
472
+ }
473
+ if (config.openaiModel === 'custom') {
474
+ finalConfig.openaiModel = customOpenAI || 'gpt-4o'
475
+ }
476
+ if (config.openrouterModel === 'custom') {
477
+ finalConfig.openrouterModel = customOpenRouter || 'openai/gpt-4o-mini'
478
+ }
479
+ if (config.deepseekModel === 'custom') {
480
+ finalConfig.deepseekModel = customDeepSeek || 'deepseek-v4-flash'
481
+ }
482
+ if (config.anthropicModel === 'custom') {
483
+ finalConfig.anthropicModel = customAnthropic || 'claude-3-5-sonnet-latest'
484
+ }
485
+ if (config.hfModel === 'custom') {
486
+ finalConfig.hfModel = customHF || 'meta-llama/Meta-Llama-3-8B-Instruct'
487
+ }
488
+ if (config.localModelName === 'custom') {
489
+ finalConfig.localModelName = customLocal || 'local-model'
490
+ }
491
+ if (config.ollamaModel === 'custom') {
492
+ finalConfig.ollamaModel = customOllama || 'llama3:latest'
493
+ }
494
+
495
+ try {
496
+ await setProfileValue('name', userName)
497
+ await setProfileValue('preferences', userPreferences)
498
+ } catch (e) {
499
+ console.error("Failed to save user profile memory:", e)
500
+ }
501
+
502
+ if (window.settingsApi) {
503
+ await window.settingsApi.saveSettings(finalConfig)
504
+ applyThemeStyles(finalConfig)
505
+ } else {
506
+ try {
507
+ await setProfileValue('user-settings', JSON.stringify(finalConfig))
508
+ applyThemeStyles(finalConfig)
509
+ } catch (e) {
510
+ console.error("Failed to save user settings:", e)
511
+ }
512
+ }
513
+ }
514
+
413
515
  const handleReset = async () => {
414
516
  if (confirm('Reset all settings to default?')) {
415
517
  setConfig(DEFAULT_CONFIG)
@@ -484,7 +586,8 @@ export default function SettingsWindow() {
484
586
  [mcpName.trim()]: {
485
587
  command: mcpCmd.trim(),
486
588
  args: argList,
487
- env: parsedEnv
589
+ env: parsedEnv,
590
+ icon: mcpIcon.trim() || undefined
488
591
  }
489
592
  }
490
593
 
@@ -497,6 +600,7 @@ export default function SettingsWindow() {
497
600
  setMcpCmd('')
498
601
  setMcpArgs('')
499
602
  setMcpEnv('')
603
+ setMcpIcon('')
500
604
  }
501
605
 
502
606
  const handleRemoveMcpServer = (name: string) => {
@@ -694,6 +798,7 @@ export default function SettingsWindow() {
694
798
  handleCheckUpdates={handleCheckUpdates}
695
799
  handleInstallUpdate={handleInstallUpdate}
696
800
  isDesktopApp={isDesktopApp}
801
+ onSaveWithoutClosing={handleSaveWithoutClosing}
697
802
  />
698
803
  )}
699
804
 
@@ -742,6 +847,8 @@ export default function SettingsWindow() {
742
847
  setMcpArgs={setMcpArgs}
743
848
  mcpEnv={mcpEnv}
744
849
  setMcpEnv={setMcpEnv}
850
+ mcpIcon={mcpIcon}
851
+ setMcpIcon={setMcpIcon}
745
852
  handleAddMcpServer={handleAddMcpServer}
746
853
  handleRemoveMcpServer={handleRemoveMcpServer}
747
854
  handleConnectPlugin={handleConnectPlugin}