@pheem49/mint 1.7.1 → 1.8.1

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 (66) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +40 -0
  5. package/Release_Note.md +95 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/crates/mint-cli/Cargo.toml +1 -0
  8. package/crates/mint-cli/src/agent.rs +142 -48
  9. package/crates/mint-cli/src/main.rs +231 -22
  10. package/crates/mint-core/src/api_server.rs +71 -4
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/chat.rs +79 -0
  13. package/crates/mint-core/src/config.rs +69 -1
  14. package/crates/mint-core/src/lib.rs +6 -1
  15. package/crates/mint-core/src/orchestration.rs +137 -1
  16. package/crates/mint-core/src/skills.rs +113 -4
  17. package/crates/mint-core/src/workflows.rs +9 -22
  18. package/package.json +2 -1
  19. package/src/renderer/src/App.tsx +1 -1
  20. package/src/renderer/src/components/ChatPanel.tsx +197 -47
  21. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  22. package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
  23. package/src/renderer/src/components/MintDashboard.tsx +239 -7
  24. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  25. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  26. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  27. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  28. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  29. package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
  30. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  31. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  32. package/src/renderer/src/components/SettingsWindow.tsx +175 -970
  33. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  34. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  35. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  36. package/src/renderer/src/css/image-studio.css +15 -4
  37. package/src/renderer/src/css/settings.css +75 -6
  38. package/src/renderer/src/css/styles.css +419 -49
  39. package/src/renderer/src/css/workflows.css +542 -0
  40. package/src/renderer/src/env.d.ts +2 -0
  41. package/src/renderer/src/index.css +13 -1
  42. package/src/renderer/src/tauri.ts +140 -2
  43. package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
  44. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  45. package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
  46. package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
  47. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  48. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  49. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  50. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  51. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  52. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
  53. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  54. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  55. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
  56. package/src/renderer/src-web/css/image-studio.css +15 -4
  57. package/src/renderer/src-web/css/settings.css +75 -6
  58. package/src/renderer/src-web/css/styles.css +394 -50
  59. package/src/renderer/src-web/env.d.ts +2 -0
  60. package/src/renderer/src-web/index.css +12 -1
  61. package/src/renderer/src-web/tauri.ts +60 -2
  62. package/src-tauri/src/browser.rs +1 -141
  63. package/src-tauri/src/headless.rs +3 -2
  64. package/src-tauri/src/lib.rs +195 -111
  65. package/src-tauri/src/workflows.rs +56 -4
  66. package/src-tauri/tauri.conf.json +1 -1
@@ -0,0 +1,632 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { DEFAULT_CONFIG } from '../SettingsWindow'
3
+ import { listLearnedSkills, addLearnedSkill, deleteLearnedSkill, LearnedSkill, detectSystemTools, DetectedTools } from '../../tauri'
4
+
5
+ interface PluginsTabProps {
6
+ config: typeof DEFAULT_CONFIG
7
+ updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
8
+ mcpName: string
9
+ setMcpName: (val: string) => void
10
+ mcpCmd: string
11
+ setMcpCmd: (val: string) => void
12
+ mcpArgs: string
13
+ setMcpArgs: (val: string) => void
14
+ mcpEnv: string
15
+ setMcpEnv: (val: string) => void
16
+ handleAddMcpServer: () => void
17
+ handleRemoveMcpServer: (name: string) => void
18
+ handleConnectPlugin: (plugin: string) => void
19
+ }
20
+
21
+ export default function PluginsTab({
22
+ config,
23
+ updateField,
24
+ mcpName,
25
+ setMcpName,
26
+ mcpCmd,
27
+ setMcpCmd,
28
+ mcpArgs,
29
+ setMcpArgs,
30
+ mcpEnv,
31
+ setMcpEnv,
32
+ handleAddMcpServer,
33
+ handleRemoveMcpServer,
34
+ handleConnectPlugin
35
+ }: PluginsTabProps) {
36
+ // Local state for learned skills
37
+ const [skills, setSkills] = useState<LearnedSkill[]>([])
38
+ const [newSkillName, setNewSkillName] = useState('')
39
+ const [newSkillContent, setNewSkillContent] = useState('')
40
+ const [skillsLoading, setSkillsLoading] = useState(false)
41
+ const [skillsError, setSkillsError] = useState('')
42
+
43
+ // Local state for auto-detected tools
44
+ const [detectedTools, setDetectedTools] = useState<DetectedTools>({
45
+ docker: false,
46
+ git: false,
47
+ gh: false,
48
+ node: false
49
+ })
50
+ const [detecting, setDetecting] = useState(false)
51
+
52
+ // Load skills and detect tools on mount
53
+ useEffect(() => {
54
+ fetchSkills()
55
+ detectTools()
56
+ }, [])
57
+
58
+ const detectTools = async () => {
59
+ setDetecting(true)
60
+ try {
61
+ const tools = await detectSystemTools()
62
+ setDetectedTools(tools)
63
+ } catch (e) {
64
+ console.error("Failed to detect tools:", e)
65
+ } finally {
66
+ setDetecting(false)
67
+ }
68
+ }
69
+
70
+ const handleEnableTool = (name: string, command: string, args: string[]) => {
71
+ const updatedMcp = {
72
+ ...(config.mcpServers || {}),
73
+ [name]: {
74
+ command,
75
+ args,
76
+ env: {}
77
+ }
78
+ }
79
+ updateField('mcpServers', updatedMcp)
80
+ }
81
+
82
+ const fetchSkills = async () => {
83
+ try {
84
+ const activeWorkspace = window.localStorage.getItem('mint:last-workspace-path') || undefined
85
+ const list = await listLearnedSkills(activeWorkspace)
86
+ setSkills(list)
87
+ } catch (e) {
88
+ console.error("Failed to load skills:", e)
89
+ }
90
+ }
91
+
92
+ const handleAddSkill = async (e: React.FormEvent) => {
93
+ e.preventDefault()
94
+ if (!newSkillName.trim() || !newSkillContent.trim()) {
95
+ setSkillsError('Please fill in both name and content.')
96
+ return
97
+ }
98
+
99
+ const cleanName = newSkillName.trim().replace(/\s+/g, '-').toLowerCase()
100
+ setSkillsLoading(true)
101
+ setSkillsError('')
102
+
103
+ try {
104
+ await addLearnedSkill(cleanName, newSkillContent)
105
+ setNewSkillName('')
106
+ setNewSkillContent('')
107
+ await fetchSkills()
108
+ } catch (err: any) {
109
+ setSkillsError(err.message || String(err))
110
+ } finally {
111
+ setSkillsLoading(false)
112
+ }
113
+ }
114
+
115
+ const handleDeleteSkill = async (name: string) => {
116
+ if (confirm(`Forget skill "${name}"?`)) {
117
+ try {
118
+ await deleteLearnedSkill(name)
119
+ await fetchSkills()
120
+ } catch (e) {
121
+ console.error("Failed to delete skill:", e)
122
+ }
123
+ }
124
+ }
125
+
126
+ // Local state to toggle expansion of configuration sections
127
+ const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null)
128
+
129
+ const toggleExpand = (pluginKey: string) => {
130
+ setExpandedPlugin(expandedPlugin === pluginKey ? null : pluginKey)
131
+ }
132
+
133
+ const pluginsList: Array<{
134
+ key: string
135
+ name: string
136
+ desc: string
137
+ icon: string
138
+ enabledField: 'pluginSpotifyEnabled' | 'pluginDiscordEnabled'
139
+ hasCredentials: boolean
140
+ fields?: Array<{ label: string; field: any; type: string; placeholder: string }>
141
+ }> = [
142
+ {
143
+ key: 'spotify',
144
+ name: 'Spotify',
145
+ desc: 'Control playback with AI. Requires playerctl locally.',
146
+ icon: '🎵',
147
+ enabledField: 'pluginSpotifyEnabled' as const,
148
+ hasCredentials: false
149
+ },
150
+ {
151
+ key: 'discord',
152
+ name: 'Discord RPC',
153
+ desc: 'Show "Using Mint Assistant" status in your local Discord client.',
154
+ icon: '💬',
155
+ enabledField: 'pluginDiscordEnabled' as const,
156
+ hasCredentials: false
157
+ }
158
+ ]
159
+
160
+ const mcpListItems: Array<{
161
+ name: string
162
+ command: string
163
+ args: string[]
164
+ icon: string
165
+ isEnabled: boolean
166
+ isConfigured: boolean
167
+ description: string
168
+ }> = []
169
+
170
+ Object.entries(config.mcpServers || {}).forEach(([name, srv]: [string, any]) => {
171
+ let icon = '⚙';
172
+ if (name === 'docker') icon = '🐳';
173
+ else if (name === 'git' || name === 'gitkraken') icon = '🐙';
174
+ else if (name === 'github') icon = '🐱';
175
+ else if (name === 'node') icon = '🟢';
176
+
177
+ mcpListItems.push({
178
+ name,
179
+ command: srv.command,
180
+ args: srv.args || [],
181
+ icon,
182
+ isEnabled: true,
183
+ isConfigured: true,
184
+ description: `Command: ${srv.command} ${(srv.args || []).join(' ')}`
185
+ });
186
+ });
187
+
188
+ if (detectedTools.docker && !config.mcpServers?.docker) {
189
+ mcpListItems.push({
190
+ name: 'docker',
191
+ command: 'npx',
192
+ args: ['-y', '@proxeus/mcp-docker-server'],
193
+ icon: '🐳',
194
+ isEnabled: false,
195
+ isConfigured: false,
196
+ description: 'Docker MCP Server (Discovered)'
197
+ });
198
+ }
199
+ if (detectedTools.git && !config.mcpServers?.git) {
200
+ mcpListItems.push({
201
+ name: 'git',
202
+ command: 'npx',
203
+ args: ['-y', '@modelcontextprotocol/server-git'],
204
+ icon: '🐙',
205
+ isEnabled: false,
206
+ isConfigured: false,
207
+ description: 'Git MCP Server (Discovered)'
208
+ });
209
+ }
210
+ if (detectedTools.gh && !config.mcpServers?.github) {
211
+ mcpListItems.push({
212
+ name: 'github',
213
+ command: 'npx',
214
+ args: ['-y', '@modelcontextprotocol/server-github'],
215
+ icon: '🐱',
216
+ isEnabled: false,
217
+ isConfigured: false,
218
+ description: 'GitHub MCP Server (Discovered)'
219
+ });
220
+ }
221
+ if (detectedTools.node && !config.mcpServers?.node) {
222
+ mcpListItems.push({
223
+ name: 'node',
224
+ command: 'npx',
225
+ args: ['-y', '@modelcontextprotocol/server-node'],
226
+ icon: '🟢',
227
+ isEnabled: false,
228
+ isConfigured: false,
229
+ description: 'NodeJS Runtime MCP Server (Discovered)'
230
+ });
231
+ }
232
+
233
+ return (
234
+ <div className="tab-pane active" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
235
+ {/* ── Learned AI Skills ── */}
236
+ <section className="setting-section">
237
+ <div className="section-heading">
238
+ <div>
239
+ <p className="section-kicker">Knowledge Base</p>
240
+ <h2 className="section-title">Learned AI Skills</h2>
241
+ </div>
242
+ <p className="section-description">
243
+ Skills are special instructions or guides taught to Mint (equivalent to <code>mint learn</code> in CLI). The AI reads active skills before every prompt to align with your guidelines.
244
+ </p>
245
+ </div>
246
+
247
+ {skills.length === 0 ? (
248
+ <div className="empty-skills-notice" style={{
249
+ padding: '24px',
250
+ border: '1px dashed var(--border)',
251
+ borderRadius: '12px',
252
+ textAlign: 'center',
253
+ color: 'var(--text-muted)',
254
+ background: 'rgba(0,0,0,0.06)',
255
+ fontSize: '0.9rem',
256
+ marginBottom: '20px'
257
+ }}>
258
+ No learned skills found. Teach Mint a skill below or run <code>/learn &lt;path&gt;</code> in chat!
259
+ </div>
260
+ ) : (
261
+ <div className="skills-list" style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
262
+ {skills.map((s) => (
263
+ <div className="skill-card" key={s.id} style={{
264
+ display: 'flex',
265
+ alignItems: 'center',
266
+ justifyContent: 'space-between',
267
+ padding: '16px 20px',
268
+ border: '1px solid var(--border)',
269
+ borderRadius: '12px',
270
+ background: 'var(--surface-bg)',
271
+ gap: '16px'
272
+ }}>
273
+ <div style={{ flex: 1, minWidth: 0 }}>
274
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
275
+ <span className="badge" style={{
276
+ background: 'var(--accent)',
277
+ color: '#fff',
278
+ fontSize: '0.75rem',
279
+ fontWeight: '600',
280
+ padding: '2px 8px',
281
+ borderRadius: '4px'
282
+ }}>{s.name}</span>
283
+ {s.location && (
284
+ <span className="location-badge" style={{
285
+ background: s.location === 'workspace'
286
+ ? 'rgba(16, 185, 129, 0.15)'
287
+ : s.location === 'global'
288
+ ? 'rgba(59, 130, 246, 0.15)'
289
+ : 'rgba(139, 92, 246, 0.15)',
290
+ color: s.location === 'workspace'
291
+ ? '#10b981'
292
+ : s.location === 'global'
293
+ ? '#3b82f6'
294
+ : '#8b5cf6',
295
+ fontSize: '0.7rem',
296
+ fontWeight: '600',
297
+ padding: '2px 6px',
298
+ borderRadius: '4px',
299
+ textTransform: 'capitalize'
300
+ }}>
301
+ {s.location === 'database' ? 'Taught' : s.location}
302
+ </span>
303
+ )}
304
+ <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}>
305
+ Source: {s.sourcePath}
306
+ </span>
307
+ </div>
308
+ </div>
309
+ <button
310
+ className="btn btn-danger"
311
+ onClick={() => handleDeleteSkill(s.name)}
312
+ style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px', flexShrink: 0 }}
313
+ >
314
+ Forget
315
+ </button>
316
+ </div>
317
+ ))}
318
+ </div>
319
+ )}
320
+
321
+ <form className="add-skill-box" onSubmit={handleAddSkill} style={{
322
+ padding: '20px',
323
+ border: '1px solid var(--border)',
324
+ borderRadius: '12px',
325
+ background: 'var(--surface-bg)'
326
+ }}>
327
+ <h3 style={{ fontSize: '1rem', fontWeight: '600', color: 'var(--text-main)', marginBottom: '14px' }}>Teach New Skill</h3>
328
+ {skillsError && <div style={{ color: '#ef4444', fontSize: '0.85rem', marginBottom: '12px', fontWeight: '500' }}>{skillsError}</div>}
329
+
330
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '12px' }}>
331
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Skill Name (e.g. coding-guidelines)</label>
332
+ <input
333
+ type="text"
334
+ placeholder="e.g. angular-standard"
335
+ value={newSkillName}
336
+ onChange={(e) => setNewSkillName(e.target.value)}
337
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
338
+ />
339
+ </div>
340
+
341
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '14px' }}>
342
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Skill Instructions / Content</label>
343
+ <textarea
344
+ placeholder="# Instructions&#10;Write only clean TypeScript. Use async/await. Avoid let where const is possible."
345
+ value={newSkillContent}
346
+ onChange={(e) => setNewSkillContent(e.target.value)}
347
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%', height: '100px', resize: 'vertical' }}
348
+ />
349
+ </div>
350
+
351
+ <button
352
+ type="submit"
353
+ className="btn-primary"
354
+ disabled={skillsLoading}
355
+ style={{ width: '100%', padding: '10px 16px', borderRadius: '8px', border: 'none', background: 'var(--accent)', color: '#fff', fontWeight: '600', cursor: 'pointer' }}
356
+ >
357
+ {skillsLoading ? 'Learning...' : 'Teach Skill'}
358
+ </button>
359
+ </form>
360
+ </section>
361
+
362
+ {/* ── External tools (MCP Servers) ── */}
363
+ <section className="setting-section" style={{ borderTop: '1px solid var(--border)', paddingTop: '24px' }}>
364
+ <div className="section-heading">
365
+ <div>
366
+ <p className="section-kicker">External tools</p>
367
+ <h2 className="section-title">MCP Servers</h2>
368
+ </div>
369
+ <p className="section-description">Connect Mint to tools like search, GitHub, or filesystem servers.</p>
370
+ </div>
371
+
372
+ <div className="mcp-list">
373
+ {mcpListItems.length === 0 ? (
374
+ <div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-muted)' }}>
375
+ No MCP servers configured or discovered.
376
+ </div>
377
+ ) : (
378
+ mcpListItems.map((item) => (
379
+ <div
380
+ className="plugin-card"
381
+ key={item.name}
382
+ style={{
383
+ display: 'flex',
384
+ alignItems: 'center',
385
+ justifyContent: 'space-between',
386
+ padding: '16px 20px',
387
+ border: '1px solid var(--border)',
388
+ borderRadius: '12px',
389
+ background: item.isConfigured ? 'var(--surface-bg)' : 'rgba(16, 185, 129, 0.03)',
390
+ opacity: item.isConfigured ? 1 : 0.65,
391
+ marginBottom: '10px',
392
+ transition: 'all 0.2s'
393
+ }}
394
+ >
395
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
396
+ <div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>
397
+ {item.icon}
398
+ </div>
399
+ <div className="plugin-info">
400
+ <div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>
401
+ {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>}
402
+ </div>
403
+ <div className="plugin-desc" style={{ fontFamily: 'monospace', fontSize: '0.8rem', color: 'var(--text-muted)' }}>
404
+ {item.description}
405
+ </div>
406
+ </div>
407
+ </div>
408
+ <div className="plugin-actions" style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
409
+ {item.isConfigured && (
410
+ <button
411
+ className="btn-icon-danger"
412
+ onClick={() => handleRemoveMcpServer(item.name)}
413
+ style={{
414
+ background: 'transparent',
415
+ border: 'none',
416
+ color: '#ef4444',
417
+ cursor: 'pointer',
418
+ padding: '6px',
419
+ display: 'flex',
420
+ alignItems: 'center',
421
+ justifyContent: 'center',
422
+ borderRadius: '6px',
423
+ transition: 'background 0.2s'
424
+ }}
425
+ title="Delete MCP Server"
426
+ >
427
+ <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">
428
+ <polyline points="3 6 5 6 21 6"></polyline>
429
+ <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>
430
+ <line x1="10" y1="11" x2="10" y2="17"></line>
431
+ <line x1="14" y1="11" x2="14" y2="17"></line>
432
+ </svg>
433
+ </button>
434
+ )}
435
+ <label className="settings-toggle-switch">
436
+ <input
437
+ type="checkbox"
438
+ checked={item.isEnabled}
439
+ onChange={(e) => {
440
+ if (e.target.checked) {
441
+ handleEnableTool(item.name, item.command, item.args);
442
+ } else {
443
+ handleRemoveMcpServer(item.name);
444
+ }
445
+ }}
446
+ />
447
+ <span className="settings-toggle-slider" />
448
+ </label>
449
+ </div>
450
+ </div>
451
+ ))
452
+ )}
453
+ </div>
454
+
455
+ {/* Save settings alert */}
456
+ {((detectedTools.docker && !config.mcpServers?.docker) ||
457
+ (detectedTools.git && !config.mcpServers?.git) ||
458
+ (detectedTools.gh && !config.mcpServers?.github) ||
459
+ (detectedTools.node && !config.mcpServers?.node)) && (
460
+ <div style={{ marginTop: '12px', padding: '12px 16px', background: 'rgba(255,193,7,0.1)', border: '1px solid rgba(255,193,7,0.3)', borderRadius: '8px', fontSize: '0.8rem', color: '#ffb300', fontWeight: '500', display: 'flex', alignItems: 'center', gap: '8px' }}>
461
+ <span>⚠️ After toggling a plugin, please click the "Save Settings" button at the bottom of the window to persist changes.</span>
462
+ </div>
463
+ )}
464
+
465
+ <div className="add-mcp-box" style={{
466
+ marginTop: '20px',
467
+ padding: '20px',
468
+ border: '1px solid var(--border)',
469
+ borderRadius: '12px',
470
+ background: 'var(--surface-bg)'
471
+ }}>
472
+ <h3 style={{ fontSize: '1rem', fontWeight: '600', color: 'var(--text-main)', marginBottom: '14px' }}>Add MCP Server</h3>
473
+ <div className="form-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
474
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
475
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Server Name</label>
476
+ <input
477
+ type="text"
478
+ placeholder="e.g. google-search"
479
+ value={mcpName}
480
+ onChange={(e) => setMcpName(e.target.value)}
481
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
482
+ />
483
+ </div>
484
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
485
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Command</label>
486
+ <input
487
+ type="text"
488
+ placeholder="e.g. npx"
489
+ value={mcpCmd}
490
+ onChange={(e) => setMcpCmd(e.target.value)}
491
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
492
+ />
493
+ </div>
494
+ </div>
495
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '12px' }}>
496
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Arguments</label>
497
+ <input
498
+ type="text"
499
+ placeholder="e.g. -y @modelcontextprotocol/server-brave-search"
500
+ value={mcpArgs}
501
+ onChange={(e) => setMcpArgs(e.target.value)}
502
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%' }}
503
+ />
504
+ </div>
505
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '14px' }}>
506
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Environment Variables (JSON)</label>
507
+ <textarea
508
+ placeholder='e.g. {"BRAVE_API_KEY": "your_key_here"}'
509
+ value={mcpEnv}
510
+ onChange={(e) => setMcpEnv(e.target.value)}
511
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%', height: '70px', resize: 'vertical' }}
512
+ />
513
+ </div>
514
+ <button className="btn-primary" onClick={handleAddMcpServer} style={{ width: '100%', padding: '10px 16px', borderRadius: '8px', border: 'none', background: 'var(--accent)', color: '#fff', fontWeight: '600', cursor: 'pointer' }}>Add MCP Server</button>
515
+ </div>
516
+ </section>
517
+
518
+ {/* ── Built-in Plugins ── */}
519
+ <section className="setting-section" style={{ borderTop: '1px solid var(--border)', paddingTop: '24px' }}>
520
+ <div className="section-heading">
521
+ <div>
522
+ <p className="section-kicker">Integrations</p>
523
+ <h2 className="section-title">Built-in Plugins</h2>
524
+ </div>
525
+ <p className="section-description">Enable and configure credentials for native Mint plugins.</p>
526
+ </div>
527
+ <div className="plugin-list" style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
528
+ {pluginsList.map(p => {
529
+ const isEnabled = config[p.enabledField]
530
+ const isExpanded = expandedPlugin === p.key
531
+
532
+ return (
533
+ <div className={`plugin-card-wrapper ${isEnabled ? 'active-plugin-card' : ''}`} key={p.key} style={{
534
+ border: '1px solid var(--border)',
535
+ borderRadius: '12px',
536
+ background: 'var(--surface-bg)',
537
+ transition: 'all 0.2s ease',
538
+ overflow: 'hidden'
539
+ }}>
540
+ <div className="plugin-card" style={{
541
+ display: 'flex',
542
+ alignItems: 'center',
543
+ justifyContent: 'space-between',
544
+ padding: '16px 20px',
545
+ background: isEnabled ? 'color-mix(in srgb, var(--accent) 3%, var(--surface-bg))' : 'transparent'
546
+ }}>
547
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
548
+ <div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>
549
+ {p.icon}
550
+ </div>
551
+ <div className="plugin-info">
552
+ <div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>{p.name}</div>
553
+ <div className="plugin-desc" style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>{p.desc}</div>
554
+ </div>
555
+ </div>
556
+ <div className="plugin-actions" style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
557
+ {p.hasCredentials && (
558
+ <button
559
+ className="btn-secondary"
560
+ onClick={() => toggleExpand(p.key)}
561
+ style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}
562
+ >
563
+ {isExpanded ? 'Hide Config' : 'Configure'}
564
+ </button>
565
+ )}
566
+ {p.key === 'discord' && isEnabled && (
567
+ <button
568
+ className="btn-secondary"
569
+ onClick={() => handleConnectPlugin('discord')}
570
+ style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}
571
+ >
572
+ Update RPC
573
+ </button>
574
+ )}
575
+ <label className="settings-toggle-switch">
576
+ <input
577
+ type="checkbox"
578
+ checked={isEnabled}
579
+ onChange={(e) => {
580
+ updateField(p.enabledField, e.target.checked)
581
+ if (!e.target.checked && expandedPlugin === p.key) {
582
+ // Don't auto-collapse config on disable so user can still edit
583
+ }
584
+ }}
585
+ />
586
+ <span className="settings-toggle-slider" />
587
+ </label>
588
+ </div>
589
+ </div>
590
+
591
+ {p.hasCredentials && isExpanded && (
592
+ <div className="plugin-config-panel" style={{
593
+ padding: '20px',
594
+ borderTop: '1px solid var(--border)',
595
+ background: 'rgba(0,0,0,0.12)',
596
+ display: 'flex',
597
+ flexDirection: 'column',
598
+ gap: '12px'
599
+ }}>
600
+ <h4 style={{ fontSize: '0.9rem', fontWeight: '600', color: 'var(--text-soft)', marginBottom: '4px' }}>Credentials Configuration</h4>
601
+ <div className="form-grid compact" style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '12px' }}>
602
+ {p.fields?.map(f => (
603
+ <div className="setting-row" key={f.field} style={{ display: 'flex', flexDirection: 'column', gap: '6px', border: 'none', padding: 0 }}>
604
+ <label style={{ fontSize: '0.8rem', fontWeight: '500', color: 'var(--text-muted)' }}>{f.label}</label>
605
+ <input
606
+ type={f.type}
607
+ placeholder={f.placeholder}
608
+ value={(config as any)[f.field] || ''}
609
+ onChange={(e) => updateField(f.field, e.target.value)}
610
+ style={{
611
+ width: '100%',
612
+ padding: '10px 14px',
613
+ borderRadius: '8px',
614
+ border: '1px solid var(--border)',
615
+ background: 'var(--input-bg)',
616
+ color: 'var(--text-main)',
617
+ fontSize: '0.9rem'
618
+ }}
619
+ />
620
+ </div>
621
+ ))}
622
+ </div>
623
+ </div>
624
+ )}
625
+ </div>
626
+ );
627
+ })}
628
+ </div>
629
+ </section>
630
+ </div>
631
+ )
632
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+
3
+ export default function ShortcutsTab() {
4
+ return (
5
+ <div className="tab-pane active">
6
+ <section className="setting-section">
7
+ <div className="section-heading">
8
+ <div>
9
+ <p className="section-kicker">Keyboard</p>
10
+ <h2 className="section-title">Shortcuts</h2>
11
+ </div>
12
+ </div>
13
+ <div className="shortcut-list">
14
+ <div className="shortcut-item">
15
+ <span>Show / Hide Mint Window</span>
16
+ <div className="keys"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>Space</kbd></div>
17
+ </div>
18
+ <div className="shortcut-item">
19
+ <span>Open Spotlight</span>
20
+ <div className="keys"><kbd>Alt</kbd><kbd>Space</kbd></div>
21
+ </div>
22
+ <div className="shortcut-item">
23
+ <span>Close / Dismiss</span>
24
+ <div className="keys"><kbd>Esc</kbd></div>
25
+ </div>
26
+ </div>
27
+ </section>
28
+ </div>
29
+ )
30
+ }