@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.
- package/.github/workflows/release.yml +1 -1
- package/Cargo.lock +34 -3
- package/Cargo.toml +2 -2
- package/README.md +40 -0
- package/Release_Note.md +95 -0
- package/assets/Mint_CLI.png +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/agent.rs +142 -48
- package/crates/mint-cli/src/main.rs +231 -22
- package/crates/mint-core/src/api_server.rs +71 -4
- package/crates/mint-core/src/browser.rs +401 -0
- package/crates/mint-core/src/chat.rs +79 -0
- package/crates/mint-core/src/config.rs +69 -1
- package/crates/mint-core/src/lib.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +137 -1
- package/crates/mint-core/src/skills.rs +113 -4
- package/crates/mint-core/src/workflows.rs +9 -22
- package/package.json +2 -1
- package/src/renderer/src/App.tsx +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +197 -47
- package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
- package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
- package/src/renderer/src/components/MintDashboard.tsx +239 -7
- package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
- package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
- package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
- package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src/components/SettingsWindow.tsx +175 -970
- package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
- package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +15 -4
- package/src/renderer/src/css/settings.css +75 -6
- package/src/renderer/src/css/styles.css +419 -49
- package/src/renderer/src/css/workflows.css +542 -0
- package/src/renderer/src/env.d.ts +2 -0
- package/src/renderer/src/index.css +13 -1
- package/src/renderer/src/tauri.ts +140 -2
- package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
- package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
- package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
- package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
- package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
- package/src/renderer/src-web/css/image-studio.css +15 -4
- package/src/renderer/src-web/css/settings.css +75 -6
- package/src/renderer/src-web/css/styles.css +394 -50
- package/src/renderer/src-web/env.d.ts +2 -0
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +60 -2
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +195 -111
- package/src-tauri/src/workflows.rs +56 -4
- 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 <path></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 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
|
+
}
|