@pheem49/mint 1.9.0 → 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 (90) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +285 -140
  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 +287 -43
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/interactive.rs +2888 -0
  10. package/crates/mint-cli/src/main.rs +336 -2854
  11. package/crates/mint-cli/src/markdown.rs +294 -0
  12. package/crates/mint-cli/src/onboard.rs +44 -1
  13. package/crates/mint-cli/src/updater.rs +48 -6
  14. package/crates/mint-core/Cargo.toml +1 -0
  15. package/crates/mint-core/src/agent_loop.rs +1 -0
  16. package/crates/mint-core/src/api_server.rs +376 -13
  17. package/crates/mint-core/src/browser.rs +629 -101
  18. package/crates/mint-core/src/channels.rs +105 -6
  19. package/crates/mint-core/src/chat.rs +52 -4
  20. package/crates/mint-core/src/code_tools.rs +29 -11
  21. package/crates/mint-core/src/config.rs +118 -0
  22. package/crates/mint-core/src/image_gen.rs +294 -33
  23. package/crates/mint-core/src/lib.rs +5 -2
  24. package/crates/mint-core/src/mcp.rs +119 -6
  25. package/crates/mint-core/src/orchestration.rs +133 -11
  26. package/crates/mint-core/src/pictures.rs +64 -1
  27. package/crates/mint-core/src/skills.rs +82 -6
  28. package/crates/mint-core/src/video_gen.rs +235 -0
  29. package/package.json +8 -2
  30. package/shared-modules/luajit/luajit.json +2 -2
  31. package/skills-lock.json +41 -0
  32. package/src/renderer/index-web.html +1 -1
  33. package/src/renderer/index.html +1 -1
  34. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  35. package/src/renderer/shared/platform.ts +3 -0
  36. package/src/renderer/shared/types.ts +4 -0
  37. package/src/renderer/shared/utils/approval.tsx +86 -7
  38. package/src/renderer/shared/utils/markdown.tsx +217 -88
  39. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  40. package/src/renderer/src/components/ChatPanel.tsx +461 -64
  41. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  42. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  43. package/src/renderer/src/components/MintDashboard.tsx +150 -5
  44. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  45. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  46. package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
  47. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  48. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  49. package/src/renderer/src/components/SettingsWindow.tsx +38 -2
  50. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  51. package/src/renderer/src/css/image-studio.css +106 -0
  52. package/src/renderer/src/css/settings.css +158 -0
  53. package/src/renderer/src/css/styles.css +352 -1
  54. package/src/renderer/src/css/veo-studio.css +626 -0
  55. package/src/renderer/src/index.css +1 -0
  56. package/src/renderer/src/tauri.ts +144 -17
  57. package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
  58. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  59. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  60. package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
  61. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  62. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  63. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
  64. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  65. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  66. package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
  67. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  68. package/src/renderer/src-web/css/image-studio.css +106 -0
  69. package/src/renderer/src-web/css/settings.css +158 -0
  70. package/src/renderer/src-web/css/styles.css +353 -1
  71. package/src/renderer/src-web/css/veo-studio.css +650 -0
  72. package/src/renderer/src-web/index.css +1 -0
  73. package/src/renderer/src-web/tauri.ts +135 -16
  74. package/src-tauri/src/lib.rs +114 -8
  75. package/src-tauri/src/webhooks.rs +45 -31
  76. package/src-tauri/src/workflows.rs +1 -0
  77. package/src-tauri/tauri.conf.json +1 -1
  78. package/temp_gridgeist/AGENTS.md +8 -0
  79. package/temp_gridgeist/CHANGELOG.md +130 -0
  80. package/temp_gridgeist/LICENSE +21 -0
  81. package/temp_gridgeist/README.md +323 -0
  82. package/temp_gridgeist/README.th.md +325 -0
  83. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  84. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  85. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  86. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  87. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  88. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  89. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  90. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -69,6 +69,33 @@ export default function GeneralTab({
69
69
  onSaveWithoutClosing
70
70
  }: GeneralTabProps) {
71
71
  const [savedProviderId, setSavedProviderId] = React.useState<string | null>(null)
72
+ const [openSections, setOpenSections] = React.useState<Record<string, boolean>>({
73
+ ai_routing: true,
74
+ search: false,
75
+ productivity: false,
76
+ image_gen: false,
77
+ video_gen: false,
78
+ custom_providers: false,
79
+ desktop_updates: false,
80
+ assistant_presence: false,
81
+ })
82
+
83
+ const toggleSection = (key: string) => {
84
+ setOpenSections(prev => ({ ...prev, [key]: !prev[key] }))
85
+ }
86
+
87
+ const setAllSections = (isOpen: boolean) => {
88
+ setOpenSections({
89
+ ai_routing: isOpen,
90
+ search: isOpen,
91
+ productivity: isOpen,
92
+ image_gen: isOpen,
93
+ video_gen: isOpen,
94
+ custom_providers: isOpen,
95
+ desktop_updates: isOpen,
96
+ assistant_presence: isOpen,
97
+ })
98
+ }
72
99
 
73
100
  const handleSaveProvider = async (providerId: string) => {
74
101
  if (onSaveWithoutClosing) {
@@ -77,467 +104,547 @@ export default function GeneralTab({
77
104
  setTimeout(() => setSavedProviderId(null), 2000)
78
105
  }
79
106
  }
80
- return (
81
- <div className="tab-pane active">
82
- <section className="setting-section">
83
- <div className="section-heading">
107
+
108
+ const renderCollapsibleSection = (
109
+ key: string,
110
+ kicker: string,
111
+ title: string,
112
+ description: string,
113
+ children: React.ReactNode,
114
+ badgeContent?: React.ReactNode
115
+ ) => {
116
+ const isOpen = openSections[key] ?? false
117
+
118
+ return (
119
+ <section
120
+ className={`setting-section collapsible-section ${isOpen ? 'is-open' : 'is-collapsed'}`}
121
+ style={{
122
+ border: '1px solid var(--border)',
123
+ borderRadius: '12px',
124
+ padding: '16px 20px',
125
+ background: 'var(--surface-bg)',
126
+ marginBottom: '14px',
127
+ transition: 'all 0.25s ease',
128
+ overflow: 'hidden'
129
+ }}
130
+ >
131
+ <div
132
+ className="section-heading-collapsible"
133
+ onClick={() => toggleSection(key)}
134
+ style={{
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ justifyContent: 'space-between',
138
+ cursor: 'pointer',
139
+ userSelect: 'none'
140
+ }}
141
+ >
84
142
  <div>
85
- <p className="section-kicker">AI routing</p>
86
- <h2 className="section-title">Provider & Model</h2>
143
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
144
+ <p className="section-kicker" style={{ margin: 0 }}>{kicker}</p>
145
+ {badgeContent}
146
+ </div>
147
+ <h2 className="section-title" style={{ margin: '2px 0 0 0', fontSize: '1.15rem' }}>{title}</h2>
148
+ {description && <p className="section-description" style={{ margin: '4px 0 0 0', fontSize: '0.82rem' }}>{description}</p>}
149
+ </div>
150
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-muted)' }}>
151
+ <span style={{ fontSize: '0.75rem', fontWeight: '500' }}>{isOpen ? 'Collapse' : 'Expand'}</span>
152
+ <svg
153
+ width="18" height="18" viewBox="0 0 24 24" fill="none"
154
+ stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
155
+ style={{
156
+ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
157
+ transition: 'transform 0.25s ease'
158
+ }}
159
+ >
160
+ <polyline points="6 9 12 15 18 9"></polyline>
161
+ </svg>
87
162
  </div>
88
- <p className="section-description">Choose which AI backend Mint uses, then set the default model.</p>
89
163
  </div>
90
164
 
91
- <div className="form-grid compact">
92
- <div className="setting-row wide">
93
- <label>Active Provider</label>
94
- <select value={config.aiProvider} onChange={(e) => updateField('aiProvider', e.target.value)}>
95
- <option value="gemini">Google Gemini (Cloud)</option>
96
- <option value="anthropic">Anthropic Claude</option>
97
- <option value="openai">OpenAI</option>
98
- <option value="openrouter">OpenRouter</option>
99
- <option value="deepseek">DeepSeek</option>
100
- <option value="ollama">Ollama (Local / Private)</option>
101
- <option value="huggingface">Hugging Face (Inference API)</option>
102
- <option value="local_openai">Local (LM Studio / OpenAI Compatible)</option>
103
- {(config.customProviders ?? []).map(cp => (
104
- <option key={`custom:${cp.id}`} value={`custom:${cp.id}`}>
105
- {cp.displayName || cp.id} (Custom)
106
- </option>
107
- ))}
108
- </select>
165
+ {isOpen && (
166
+ <div className="section-collapsible-body" style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border)' }}>
167
+ {children}
109
168
  </div>
110
- {/* Model selector for custom providers */}
111
- {config.aiProvider.startsWith('custom:') && (() => {
112
- const activeId = config.aiProvider.replace(/^custom:/, '')
113
- const cp = (config.customProviders ?? []).find(p => p.id === activeId)
114
- if (!cp || cp.models.length === 0) return null
115
- const currentModel = (config.customModelSelections ?? {})[activeId] ?? cp.models[0]?.modelId ?? ''
116
- return (
117
- <div className="setting-row wide">
118
- <label>Active Model</label>
119
- <select
120
- value={currentModel}
121
- onChange={(e) => updateField('customModelSelections', {
122
- ...(config.customModelSelections ?? {}),
123
- [activeId]: e.target.value
124
- })}
125
- >
126
- {cp.models.map(m => (
127
- <option key={m.modelId} value={m.modelId}>
128
- {m.displayName || m.modelId}
129
- </option>
130
- ))}
131
- </select>
132
- </div>
133
- )
134
- })()}
135
- </div>
169
+ )}
170
+ </section>
171
+ )
172
+ }
136
173
 
137
- <div className="provider-cards-container">
138
- {/* Google Gemini Card */}
139
- <div className={`provider-card ${config.aiProvider === 'gemini' ? 'active-provider' : ''}`}>
140
- <div className="provider-card-header">
141
- <div className="provider-card-title">
142
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
143
- <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
144
- </svg>
145
- Google Gemini (Cloud)
146
- </div>
147
- {config.aiProvider === 'gemini' && <span className="provider-active-badge">Active</span>}
174
+ return (
175
+ <div className="tab-pane active">
176
+ {/* Accordion Quick Control Bar */}
177
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginBottom: '14px' }}>
178
+ <button
179
+ className="btn-secondary"
180
+ onClick={() => setAllSections(true)}
181
+ style={{ fontSize: '0.78rem', padding: '4px 10px', borderRadius: '6px', cursor: 'pointer', border: '1px solid var(--border)', background: 'var(--surface-strong)', color: 'var(--text-main)' }}
182
+ >
183
+ Expand All
184
+ </button>
185
+ <button
186
+ className="btn-secondary"
187
+ onClick={() => setAllSections(false)}
188
+ style={{ fontSize: '0.78rem', padding: '4px 10px', borderRadius: '6px', cursor: 'pointer', border: '1px solid var(--border)', background: 'var(--surface-strong)', color: 'var(--text-main)' }}
189
+ >
190
+ Collapse All
191
+ </button>
192
+ </div>
193
+
194
+ {/* ── Section 1: AI Routing ── */}
195
+ {renderCollapsibleSection(
196
+ 'ai_routing',
197
+ 'AI routing',
198
+ 'Provider & Model',
199
+ 'Choose which AI backend Mint uses, then set the default model.',
200
+ <>
201
+ <div className="form-grid compact">
202
+ <div className="setting-row wide">
203
+ <label>Active Provider</label>
204
+ <select value={config.aiProvider} onChange={(e) => updateField('aiProvider', e.target.value)}>
205
+ <option value="gemini">Google Gemini (Cloud)</option>
206
+ <option value="anthropic">Anthropic Claude</option>
207
+ <option value="openai">OpenAI</option>
208
+ <option value="openrouter">OpenRouter</option>
209
+ <option value="deepseek">DeepSeek</option>
210
+ <option value="ollama">Ollama (Local / Private)</option>
211
+ <option value="huggingface">Hugging Face (Inference API)</option>
212
+ <option value="local_openai">Local (LM Studio / OpenAI Compatible)</option>
213
+ {(config.customProviders ?? []).map(cp => (
214
+ <option key={`custom:${cp.id}`} value={`custom:${cp.id}`}>
215
+ {cp.displayName || cp.id} (Custom)
216
+ </option>
217
+ ))}
218
+ </select>
148
219
  </div>
149
- <div className="provider-card-body">
150
- <div className="setting-row">
151
- <label>Gemini Model</label>
152
- <select
153
- value={GEMINI_MODELS.includes(config.geminiModel) ? config.geminiModel : 'custom'}
154
- onChange={(e) => updateField('geminiModel', e.target.value)}
155
- >
156
- {GEMINI_MODELS.map(model => (
157
- <option key={model} value={model}>{model}</option>
158
- ))}
159
- <option value="custom">Custom...</option>
160
- </select>
220
+ {config.aiProvider.startsWith('custom:') && (() => {
221
+ const activeId = config.aiProvider.replace(/^custom:/, '')
222
+ const cp = (config.customProviders ?? []).find(p => p.id === activeId)
223
+ if (!cp || cp.models.length === 0) return null
224
+ const currentModel = (config.customModelSelections ?? {})[activeId] ?? cp.models[0]?.modelId ?? ''
225
+ return (
226
+ <div className="setting-row wide">
227
+ <label>Active Model</label>
228
+ <select
229
+ value={currentModel}
230
+ onChange={(e) => updateField('customModelSelections', {
231
+ ...(config.customModelSelections ?? {}),
232
+ [activeId]: e.target.value
233
+ })}
234
+ >
235
+ {cp.models.map(m => (
236
+ <option key={m.modelId} value={m.modelId}>
237
+ {m.displayName || m.modelId}
238
+ </option>
239
+ ))}
240
+ </select>
241
+ </div>
242
+ )
243
+ })()}
244
+ </div>
245
+
246
+ <div className="provider-cards-container">
247
+ {/* Google Gemini Card */}
248
+ <div className={`provider-card ${config.aiProvider === 'gemini' ? 'active-provider' : ''}`}>
249
+ <div className="provider-card-header">
250
+ <div className="provider-card-title">
251
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
252
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
253
+ </svg>
254
+ Google Gemini (Cloud)
255
+ </div>
256
+ {config.aiProvider === 'gemini' && <span className="provider-active-badge">Active</span>}
161
257
  </div>
162
- {(!GEMINI_MODELS.includes(config.geminiModel) || config.geminiModel === 'custom') && (
258
+ <div className="provider-card-body">
163
259
  <div className="setting-row">
164
- <label>Custom Gemini Model</label>
260
+ <label>Gemini Model</label>
261
+ <select
262
+ value={GEMINI_MODELS.includes(config.geminiModel) ? config.geminiModel : 'custom'}
263
+ onChange={(e) => updateField('geminiModel', e.target.value)}
264
+ >
265
+ {GEMINI_MODELS.map(model => (
266
+ <option key={model} value={model}>{model}</option>
267
+ ))}
268
+ <option value="custom">Custom...</option>
269
+ </select>
270
+ </div>
271
+ {(!GEMINI_MODELS.includes(config.geminiModel) || config.geminiModel === 'custom') && (
272
+ <div className="setting-row">
273
+ <label>Custom Gemini Model</label>
274
+ <input
275
+ type="text"
276
+ value={customGemini}
277
+ onChange={(e) => { setCustomGemini(e.target.value); updateField('geminiModel', 'custom') }}
278
+ placeholder="e.g. gemini-3.1-flash-lite-preview"
279
+ />
280
+ </div>
281
+ )}
282
+ <div className="setting-row">
283
+ <label>Gemini API Key</label>
165
284
  <input
166
- type="text"
167
- value={customGemini}
168
- onChange={(e) => { setCustomGemini(e.target.value); updateField('geminiModel', 'custom') }}
169
- placeholder="e.g. gemini-3.1-flash-lite-preview"
285
+ type="password"
286
+ value={config.apiKey}
287
+ onChange={(e) => updateField('apiKey', e.target.value)}
288
+ placeholder="Enter Gemini API Key..."
170
289
  />
171
290
  </div>
172
- )}
173
- <div className="setting-row">
174
- <label>Gemini API Key</label>
175
- <input
176
- type="password"
177
- value={config.apiKey}
178
- onChange={(e) => updateField('apiKey', e.target.value)}
179
- placeholder="Enter Gemini API Key..."
180
- />
181
291
  </div>
182
292
  </div>
183
- </div>
184
293
 
185
- {/* Anthropic Claude Card */}
186
- <div className={`provider-card ${config.aiProvider === 'anthropic' ? 'active-provider' : ''}`}>
187
- <div className="provider-card-header">
188
- <div className="provider-card-title">
189
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
190
- <path d="M4.5 16.5c-1.5 1.26-2.5 3.19-2.5 5.5h20c0-2.31-1-4.24-2.5-5.5"></path>
191
- <path d="M12 2L2 22h20L12 2z"></path>
192
- </svg>
193
- Anthropic Claude
194
- </div>
195
- {config.aiProvider === 'anthropic' && <span className="provider-active-badge">Active</span>}
196
- </div>
197
- <div className="provider-card-body">
198
- <div className="setting-row">
199
- <label>Anthropic Model</label>
200
- <select
201
- value={ANTHROPIC_MODELS.includes(config.anthropicModel) ? config.anthropicModel : 'custom'}
202
- onChange={(e) => updateField('anthropicModel', e.target.value)}
203
- >
204
- {ANTHROPIC_MODELS.map(model => (
205
- <option key={model} value={model}>{model}</option>
206
- ))}
207
- <option value="custom">Custom...</option>
208
- </select>
294
+ {/* Anthropic Claude Card */}
295
+ <div className={`provider-card ${config.aiProvider === 'anthropic' ? 'active-provider' : ''}`}>
296
+ <div className="provider-card-header">
297
+ <div className="provider-card-title">
298
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
299
+ <path d="M4.5 16.5c-1.5 1.26-2.5 3.19-2.5 5.5h20c0-2.31-1-4.24-2.5-5.5"></path>
300
+ <path d="M12 2L2 22h20L12 2z"></path>
301
+ </svg>
302
+ Anthropic Claude
303
+ </div>
304
+ {config.aiProvider === 'anthropic' && <span className="provider-active-badge">Active</span>}
209
305
  </div>
210
- {(!ANTHROPIC_MODELS.includes(config.anthropicModel) || config.anthropicModel === 'custom') && (
306
+ <div className="provider-card-body">
307
+ <div className="setting-row">
308
+ <label>Anthropic Model</label>
309
+ <select
310
+ value={ANTHROPIC_MODELS.includes(config.anthropicModel) ? config.anthropicModel : 'custom'}
311
+ onChange={(e) => updateField('anthropicModel', e.target.value)}
312
+ >
313
+ {ANTHROPIC_MODELS.map(model => (
314
+ <option key={model} value={model}>{model}</option>
315
+ ))}
316
+ <option value="custom">Custom...</option>
317
+ </select>
318
+ </div>
319
+ {(!ANTHROPIC_MODELS.includes(config.anthropicModel) || config.anthropicModel === 'custom') && (
320
+ <div className="setting-row">
321
+ <label>Custom Anthropic Model</label>
322
+ <input
323
+ type="text"
324
+ value={customAnthropic}
325
+ onChange={(e) => { setCustomAnthropic(e.target.value); updateField('anthropicModel', 'custom') }}
326
+ placeholder="e.g. claude-3-5-sonnet-latest"
327
+ />
328
+ </div>
329
+ )}
211
330
  <div className="setting-row">
212
- <label>Custom Anthropic Model</label>
331
+ <label>Anthropic API Key</label>
213
332
  <input
214
- type="text"
215
- value={customAnthropic}
216
- onChange={(e) => { setCustomAnthropic(e.target.value); updateField('anthropicModel', 'custom') }}
217
- placeholder="e.g. claude-3-5-sonnet-latest"
333
+ type="password"
334
+ value={config.anthropicApiKey}
335
+ onChange={(e) => updateField('anthropicApiKey', e.target.value)}
336
+ placeholder="Enter Anthropic API Key..."
218
337
  />
219
338
  </div>
220
- )}
221
- <div className="setting-row">
222
- <label>Anthropic API Key</label>
223
- <input
224
- type="password"
225
- value={config.anthropicApiKey}
226
- onChange={(e) => updateField('anthropicApiKey', e.target.value)}
227
- placeholder="Enter Anthropic API Key..."
228
- />
229
339
  </div>
230
340
  </div>
231
- </div>
232
341
 
233
- {/* OpenAI Card */}
234
- <div className={`provider-card ${config.aiProvider === 'openai' ? 'active-provider' : ''}`}>
235
- <div className="provider-card-header">
236
- <div className="provider-card-title">
237
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
238
- <circle cx="12" cy="12" r="10"></circle>
239
- <line x1="12" y1="2" x2="12" y2="22"></line>
240
- <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
241
- </svg>
242
- OpenAI
243
- </div>
244
- {config.aiProvider === 'openai' && <span className="provider-active-badge">Active</span>}
245
- </div>
246
- <div className="provider-card-body">
247
- <div className="setting-row">
248
- <label>OpenAI Model</label>
249
- <select
250
- value={OPENAI_MODELS.includes(config.openaiModel) ? config.openaiModel : 'custom'}
251
- onChange={(e) => updateField('openaiModel', e.target.value)}
252
- >
253
- {OPENAI_MODELS.map(model => (
254
- <option key={model} value={model}>{model}</option>
255
- ))}
256
- <option value="custom">Custom...</option>
257
- </select>
342
+ {/* OpenAI Card */}
343
+ <div className={`provider-card ${config.aiProvider === 'openai' ? 'active-provider' : ''}`}>
344
+ <div className="provider-card-header">
345
+ <div className="provider-card-title">
346
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
347
+ <circle cx="12" cy="12" r="10"></circle>
348
+ <line x1="12" y1="2" x2="12" y2="22"></line>
349
+ <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
350
+ </svg>
351
+ OpenAI
352
+ </div>
353
+ {config.aiProvider === 'openai' && <span className="provider-active-badge">Active</span>}
258
354
  </div>
259
- {(!OPENAI_MODELS.includes(config.openaiModel) || config.openaiModel === 'custom') && (
355
+ <div className="provider-card-body">
356
+ <div className="setting-row">
357
+ <label>OpenAI Model</label>
358
+ <select
359
+ value={OPENAI_MODELS.includes(config.openaiModel) ? config.openaiModel : 'custom'}
360
+ onChange={(e) => updateField('openaiModel', e.target.value)}
361
+ >
362
+ {OPENAI_MODELS.map(model => (
363
+ <option key={model} value={model}>{model}</option>
364
+ ))}
365
+ <option value="custom">Custom...</option>
366
+ </select>
367
+ </div>
368
+ {(!OPENAI_MODELS.includes(config.openaiModel) || config.openaiModel === 'custom') && (
369
+ <div className="setting-row">
370
+ <label>Custom OpenAI Model</label>
371
+ <input
372
+ type="text"
373
+ value={customOpenAI}
374
+ onChange={(e) => { setCustomOpenAI(e.target.value); updateField('openaiModel', 'custom') }}
375
+ placeholder="e.g. gpt-4o"
376
+ />
377
+ </div>
378
+ )}
260
379
  <div className="setting-row">
261
- <label>Custom OpenAI Model</label>
380
+ <label>OpenAI API Key</label>
262
381
  <input
263
- type="text"
264
- value={customOpenAI}
265
- onChange={(e) => { setCustomOpenAI(e.target.value); updateField('openaiModel', 'custom') }}
266
- placeholder="e.g. gpt-4o"
382
+ type="password"
383
+ value={config.openaiApiKey}
384
+ onChange={(e) => updateField('openaiApiKey', e.target.value)}
385
+ placeholder="Enter OpenAI API Key..."
267
386
  />
268
387
  </div>
269
- )}
270
- <div className="setting-row">
271
- <label>OpenAI API Key</label>
272
- <input
273
- type="password"
274
- value={config.openaiApiKey}
275
- onChange={(e) => updateField('openaiApiKey', e.target.value)}
276
- placeholder="Enter OpenAI API Key..."
277
- />
278
388
  </div>
279
389
  </div>
280
- </div>
281
390
 
282
- {/* OpenRouter Card */}
283
- <div className={`provider-card ${config.aiProvider === 'openrouter' ? 'active-provider' : ''}`}>
284
- <div className="provider-card-header">
285
- <div className="provider-card-title">
286
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
287
- <polyline points="16 3 21 3 21 8"></polyline>
288
- <line x1="4" y1="20" x2="21" y2="3"></line>
289
- <polyline points="21 16 21 21 16 21"></polyline>
290
- <line x1="15" y1="15" x2="21" y2="21"></line>
291
- <line x1="4" y1="4" x2="9" y2="9"></line>
292
- </svg>
293
- OpenRouter
294
- </div>
295
- {config.aiProvider === 'openrouter' && <span className="provider-active-badge">Active</span>}
296
- </div>
297
- <div className="provider-card-body">
298
- <div className="setting-row">
299
- <label>OpenRouter Model</label>
300
- <select
301
- value={OPENROUTER_MODELS.includes(config.openrouterModel) ? config.openrouterModel : 'custom'}
302
- onChange={(e) => updateField('openrouterModel', e.target.value)}
303
- >
304
- {OPENROUTER_MODELS.map(model => (
305
- <option key={model} value={model}>{model}</option>
306
- ))}
307
- <option value="custom">Custom...</option>
308
- </select>
391
+ {/* OpenRouter Card */}
392
+ <div className={`provider-card ${config.aiProvider === 'openrouter' ? 'active-provider' : ''}`}>
393
+ <div className="provider-card-header">
394
+ <div className="provider-card-title">
395
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
396
+ <polyline points="16 3 21 3 21 8"></polyline>
397
+ <line x1="4" y1="20" x2="21" y2="3"></line>
398
+ <polyline points="21 16 21 21 16 21"></polyline>
399
+ <line x1="15" y1="15" x2="21" y2="21"></line>
400
+ <line x1="4" y1="4" x2="9" y2="9"></line>
401
+ </svg>
402
+ OpenRouter
403
+ </div>
404
+ {config.aiProvider === 'openrouter' && <span className="provider-active-badge">Active</span>}
309
405
  </div>
310
- {(!OPENROUTER_MODELS.includes(config.openrouterModel) || config.openrouterModel === 'custom') && (
406
+ <div className="provider-card-body">
407
+ <div className="setting-row">
408
+ <label>OpenRouter Model</label>
409
+ <select
410
+ value={OPENROUTER_MODELS.includes(config.openrouterModel) ? config.openrouterModel : 'custom'}
411
+ onChange={(e) => updateField('openrouterModel', e.target.value)}
412
+ >
413
+ {OPENROUTER_MODELS.map(model => (
414
+ <option key={model} value={model}>{model}</option>
415
+ ))}
416
+ <option value="custom">Custom...</option>
417
+ </select>
418
+ </div>
419
+ {(!OPENROUTER_MODELS.includes(config.openrouterModel) || config.openrouterModel === 'custom') && (
420
+ <div className="setting-row">
421
+ <label>Custom OpenRouter Model</label>
422
+ <input
423
+ type="text"
424
+ value={customOpenRouter}
425
+ onChange={(e) => { setCustomOpenRouter(e.target.value); updateField('openrouterModel', 'custom') }}
426
+ placeholder="e.g. anthropic/claude-3.5-sonnet"
427
+ />
428
+ </div>
429
+ )}
311
430
  <div className="setting-row">
312
- <label>Custom OpenRouter Model</label>
431
+ <label>OpenRouter API Key</label>
313
432
  <input
314
- type="text"
315
- value={customOpenRouter}
316
- onChange={(e) => { setCustomOpenRouter(e.target.value); updateField('openrouterModel', 'custom') }}
317
- placeholder="e.g. anthropic/claude-3.5-sonnet"
433
+ type="password"
434
+ value={config.openrouterApiKey}
435
+ onChange={(e) => updateField('openrouterApiKey', e.target.value)}
436
+ placeholder="Enter OpenRouter API Key..."
318
437
  />
319
438
  </div>
320
- )}
321
- <div className="setting-row">
322
- <label>OpenRouter API Key</label>
323
- <input
324
- type="password"
325
- value={config.openrouterApiKey}
326
- onChange={(e) => updateField('openrouterApiKey', e.target.value)}
327
- placeholder="Enter OpenRouter API Key..."
328
- />
329
439
  </div>
330
440
  </div>
331
- </div>
332
441
 
333
- {/* DeepSeek Card */}
334
- <div className={`provider-card ${config.aiProvider === 'deepseek' ? 'active-provider' : ''}`}>
335
- <div className="provider-card-header">
336
- <div className="provider-card-title">
337
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
338
- <circle cx="11" cy="11" r="8"></circle>
339
- <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
340
- </svg>
341
- DeepSeek
342
- </div>
343
- {config.aiProvider === 'deepseek' && <span className="provider-active-badge">Active</span>}
344
- </div>
345
- <div className="provider-card-body">
346
- <div className="setting-row">
347
- <label>DeepSeek Model</label>
348
- <select
349
- value={DEEPSEEK_MODELS.includes(config.deepseekModel) ? config.deepseekModel : 'custom'}
350
- onChange={(e) => updateField('deepseekModel', e.target.value)}
351
- >
352
- {DEEPSEEK_MODELS.map(model => (
353
- <option key={model} value={model}>{model}</option>
354
- ))}
355
- <option value="custom">Custom...</option>
356
- </select>
442
+ {/* DeepSeek Card */}
443
+ <div className={`provider-card ${config.aiProvider === 'deepseek' ? 'active-provider' : ''}`}>
444
+ <div className="provider-card-header">
445
+ <div className="provider-card-title">
446
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
447
+ <circle cx="11" cy="11" r="8"></circle>
448
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
449
+ </svg>
450
+ DeepSeek
451
+ </div>
452
+ {config.aiProvider === 'deepseek' && <span className="provider-active-badge">Active</span>}
357
453
  </div>
358
- {(!DEEPSEEK_MODELS.includes(config.deepseekModel) || config.deepseekModel === 'custom') && (
454
+ <div className="provider-card-body">
455
+ <div className="setting-row">
456
+ <label>DeepSeek Model</label>
457
+ <select
458
+ value={DEEPSEEK_MODELS.includes(config.deepseekModel) ? config.deepseekModel : 'custom'}
459
+ onChange={(e) => updateField('deepseekModel', e.target.value)}
460
+ >
461
+ {DEEPSEEK_MODELS.map(model => (
462
+ <option key={model} value={model}>{model}</option>
463
+ ))}
464
+ <option value="custom">Custom...</option>
465
+ </select>
466
+ </div>
467
+ {(!DEEPSEEK_MODELS.includes(config.deepseekModel) || config.deepseekModel === 'custom') && (
468
+ <div className="setting-row">
469
+ <label>Custom DeepSeek Model</label>
470
+ <input
471
+ type="text"
472
+ value={customDeepSeek}
473
+ onChange={(e) => { setCustomDeepSeek(e.target.value); updateField('deepseekModel', 'custom') }}
474
+ placeholder="e.g. deepseek-v4-pro"
475
+ />
476
+ </div>
477
+ )}
359
478
  <div className="setting-row">
360
- <label>Custom DeepSeek Model</label>
479
+ <label>DeepSeek API Key</label>
361
480
  <input
362
- type="text"
363
- value={customDeepSeek}
364
- onChange={(e) => { setCustomDeepSeek(e.target.value); updateField('deepseekModel', 'custom') }}
365
- placeholder="e.g. deepseek-v4-pro"
481
+ type="password"
482
+ value={config.deepseekApiKey}
483
+ onChange={(e) => updateField('deepseekApiKey', e.target.value)}
484
+ placeholder="Enter DeepSeek API Key..."
366
485
  />
367
486
  </div>
368
- )}
369
- <div className="setting-row">
370
- <label>DeepSeek API Key</label>
371
- <input
372
- type="password"
373
- value={config.deepseekApiKey}
374
- onChange={(e) => updateField('deepseekApiKey', e.target.value)}
375
- placeholder="Enter DeepSeek API Key..."
376
- />
377
487
  </div>
378
488
  </div>
379
- </div>
380
489
 
381
- {/* Hugging Face Card */}
382
- <div className={`provider-card ${config.aiProvider === 'huggingface' ? 'active-provider' : ''}`}>
383
- <div className="provider-card-header">
384
- <div className="provider-card-title">
385
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
386
- <circle cx="12" cy="12" r="10"></circle>
387
- <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
388
- <line x1="9" y1="9" x2="9.01" y2="9"></line>
389
- <line x1="15" y1="9" x2="15.01" y2="9"></line>
390
- </svg>
391
- Hugging Face (Inference API)
392
- </div>
393
- {config.aiProvider === 'huggingface' && <span className="provider-active-badge">Active</span>}
394
- </div>
395
- <div className="provider-card-body">
396
- <div className="setting-row">
397
- <label>Hugging Face Model</label>
398
- <select
399
- value={HF_MODELS.includes(config.hfModel) ? config.hfModel : 'custom'}
400
- onChange={(e) => updateField('hfModel', e.target.value)}
401
- >
402
- {HF_MODELS.map(model => (
403
- <option key={model} value={model}>{model}</option>
404
- ))}
405
- <option value="custom">Custom...</option>
406
- </select>
490
+ {/* Hugging Face Card */}
491
+ <div className={`provider-card ${config.aiProvider === 'huggingface' ? 'active-provider' : ''}`}>
492
+ <div className="provider-card-header">
493
+ <div className="provider-card-title">
494
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
495
+ <circle cx="12" cy="12" r="10"></circle>
496
+ <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
497
+ <line x1="9" y1="9" x2="9.01" y2="9"></line>
498
+ <line x1="15" y1="9" x2="15.01" y2="9"></line>
499
+ </svg>
500
+ Hugging Face (Inference API)
501
+ </div>
502
+ {config.aiProvider === 'huggingface' && <span className="provider-active-badge">Active</span>}
407
503
  </div>
408
- {(!HF_MODELS.includes(config.hfModel) || config.hfModel === 'custom') && (
504
+ <div className="provider-card-body">
505
+ <div className="setting-row">
506
+ <label>Hugging Face Model</label>
507
+ <select
508
+ value={HF_MODELS.includes(config.hfModel) ? config.hfModel : 'custom'}
509
+ onChange={(e) => updateField('hfModel', e.target.value)}
510
+ >
511
+ {HF_MODELS.map(model => (
512
+ <option key={model} value={model}>{model}</option>
513
+ ))}
514
+ <option value="custom">Custom...</option>
515
+ </select>
516
+ </div>
517
+ {(!HF_MODELS.includes(config.hfModel) || config.hfModel === 'custom') && (
518
+ <div className="setting-row">
519
+ <label>Custom Hugging Face Model</label>
520
+ <input
521
+ type="text"
522
+ value={customHF}
523
+ onChange={(e) => { setCustomHF(e.target.value); updateField('hfModel', 'custom') }}
524
+ placeholder="e.g. meta-llama/Meta-Llama-3-8B-Instruct"
525
+ />
526
+ </div>
527
+ )}
409
528
  <div className="setting-row">
410
- <label>Custom Hugging Face Model</label>
529
+ <label>Hugging Face API Key</label>
411
530
  <input
412
- type="text"
413
- value={customHF}
414
- onChange={(e) => { setCustomHF(e.target.value); updateField('hfModel', 'custom') }}
415
- placeholder="e.g. meta-llama/Meta-Llama-3-8B-Instruct"
531
+ type="password"
532
+ value={config.hfApiKey}
533
+ onChange={(e) => updateField('hfApiKey', e.target.value)}
534
+ placeholder="Enter Hugging Face API Key..."
416
535
  />
417
536
  </div>
418
- )}
419
- <div className="setting-row">
420
- <label>Hugging Face API Key</label>
421
- <input
422
- type="password"
423
- value={config.hfApiKey}
424
- onChange={(e) => updateField('hfApiKey', e.target.value)}
425
- placeholder="Enter Hugging Face API Key..."
426
- />
427
537
  </div>
428
538
  </div>
429
- </div>
430
539
 
431
- {/* LM Studio Card */}
432
- <div className={`provider-card ${config.aiProvider === 'local_openai' ? 'active-provider' : ''}`}>
433
- <div className="provider-card-header">
434
- <div className="provider-card-title">
435
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
436
- <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
437
- <line x1="8" y1="21" x2="16" y2="21"></line>
438
- <line x1="12" y1="17" x2="12" y2="21"></line>
439
- </svg>
440
- LM Studio / Local OpenAI
441
- </div>
442
- {config.aiProvider === 'local_openai' && <span className="provider-active-badge">Active</span>}
443
- </div>
444
- <div className="provider-card-body">
445
- <div className="setting-row">
446
- <label>LM Studio Model</label>
447
- <select
448
- value={LOCAL_MODELS.includes(config.localModelName) ? config.localModelName : 'custom'}
449
- onChange={(e) => updateField('localModelName', e.target.value)}
450
- >
451
- {LOCAL_MODELS.map(model => (
452
- <option key={model} value={model}>{model}</option>
453
- ))}
454
- <option value="custom">Custom...</option>
455
- </select>
540
+ {/* LM Studio Card */}
541
+ <div className={`provider-card ${config.aiProvider === 'local_openai' ? 'active-provider' : ''}`}>
542
+ <div className="provider-card-header">
543
+ <div className="provider-card-title">
544
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
545
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
546
+ <line x1="8" y1="21" x2="16" y2="21"></line>
547
+ <line x1="12" y1="17" x2="12" y2="21"></line>
548
+ </svg>
549
+ LM Studio / Local OpenAI
550
+ </div>
551
+ {config.aiProvider === 'local_openai' && <span className="provider-active-badge">Active</span>}
456
552
  </div>
457
- {(!LOCAL_MODELS.includes(config.localModelName) || config.localModelName === 'custom') && (
553
+ <div className="provider-card-body">
554
+ <div className="setting-row">
555
+ <label>LM Studio Model</label>
556
+ <select
557
+ value={LOCAL_MODELS.includes(config.localModelName) ? config.localModelName : 'custom'}
558
+ onChange={(e) => updateField('localModelName', e.target.value)}
559
+ >
560
+ {LOCAL_MODELS.map(model => (
561
+ <option key={model} value={model}>{model}</option>
562
+ ))}
563
+ <option value="custom">Custom...</option>
564
+ </select>
565
+ </div>
566
+ {(!LOCAL_MODELS.includes(config.localModelName) || config.localModelName === 'custom') && (
567
+ <div className="setting-row">
568
+ <label>Custom LM Studio Model</label>
569
+ <input
570
+ type="text"
571
+ value={customLocal}
572
+ onChange={(e) => { setCustomLocal(e.target.value); updateField('localModelName', 'custom') }}
573
+ placeholder="e.g. local-model"
574
+ />
575
+ </div>
576
+ )}
458
577
  <div className="setting-row">
459
- <label>Custom LM Studio Model</label>
578
+ <label>LM Studio Base URL</label>
460
579
  <input
461
580
  type="text"
462
- value={customLocal}
463
- onChange={(e) => { setCustomLocal(e.target.value); updateField('localModelName', 'custom') }}
464
- placeholder="e.g. local-model"
581
+ value={config.localApiBaseUrl}
582
+ onChange={(e) => updateField('localApiBaseUrl', e.target.value)}
583
+ placeholder="e.g. http://localhost:1234/v1"
465
584
  />
466
585
  </div>
467
- )}
468
- <div className="setting-row">
469
- <label>LM Studio Base URL</label>
470
- <input
471
- type="text"
472
- value={config.localApiBaseUrl}
473
- onChange={(e) => updateField('localApiBaseUrl', e.target.value)}
474
- placeholder="e.g. http://localhost:1234/v1"
475
- />
476
586
  </div>
477
587
  </div>
478
- </div>
479
588
 
480
- {/* Ollama Card */}
481
- <div className={`provider-card ${config.aiProvider === 'ollama' ? 'active-provider' : ''}`}>
482
- <div className="provider-card-header">
483
- <div className="provider-card-title">
484
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
485
- <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
486
- </svg>
487
- Ollama (Local)
488
- </div>
489
- {config.aiProvider === 'ollama' && <span className="provider-active-badge">Active</span>}
490
- </div>
491
- <div className="provider-card-body">
492
- <div className="setting-row">
493
- <label>Ollama Model</label>
494
- <select
495
- value={dynamicOllamaModels.includes(config.ollamaModel) ? config.ollamaModel : 'custom'}
496
- onChange={(e) => updateField('ollamaModel', e.target.value)}
497
- >
498
- {dynamicOllamaModels.map(model => (
499
- <option key={model} value={model}>{model}</option>
500
- ))}
501
- {!dynamicOllamaModels.includes(config.ollamaModel) && config.ollamaModel && config.ollamaModel !== 'custom' && (
502
- <option value={config.ollamaModel}>{config.ollamaModel}</option>
503
- )}
504
- <option value="custom">Custom...</option>
505
- </select>
589
+ {/* Ollama Card */}
590
+ <div className={`provider-card ${config.aiProvider === 'ollama' ? 'active-provider' : ''}`}>
591
+ <div className="provider-card-header">
592
+ <div className="provider-card-title">
593
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
594
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
595
+ </svg>
596
+ Ollama (Local)
597
+ </div>
598
+ {config.aiProvider === 'ollama' && <span className="provider-active-badge">Active</span>}
506
599
  </div>
507
- {(!dynamicOllamaModels.includes(config.ollamaModel) || config.ollamaModel === 'custom') && (
600
+ <div className="provider-card-body">
508
601
  <div className="setting-row">
509
- <label>Custom Ollama Model</label>
602
+ <label>Ollama Model</label>
603
+ <select
604
+ value={dynamicOllamaModels.includes(config.ollamaModel) ? config.ollamaModel : 'custom'}
605
+ onChange={(e) => updateField('ollamaModel', e.target.value)}
606
+ >
607
+ {dynamicOllamaModels.map(model => (
608
+ <option key={model} value={model}>{model}</option>
609
+ ))}
610
+ {!dynamicOllamaModels.includes(config.ollamaModel) && config.ollamaModel && config.ollamaModel !== 'custom' && (
611
+ <option value={config.ollamaModel}>{config.ollamaModel}</option>
612
+ )}
613
+ <option value="custom">Custom...</option>
614
+ </select>
615
+ </div>
616
+ {(!dynamicOllamaModels.includes(config.ollamaModel) || config.ollamaModel === 'custom') && (
617
+ <div className="setting-row">
618
+ <label>Custom Ollama Model</label>
619
+ <input
620
+ type="text"
621
+ value={customOllama}
622
+ onChange={(e) => { setCustomOllama(e.target.value); updateField('ollamaModel', 'custom') }}
623
+ placeholder="e.g. llama3:latest"
624
+ />
625
+ </div>
626
+ )}
627
+ <div className="setting-row">
628
+ <label>Ollama Host</label>
510
629
  <input
511
630
  type="text"
512
- value={customOllama}
513
- onChange={(e) => { setCustomOllama(e.target.value); updateField('ollamaModel', 'custom') }}
514
- placeholder="e.g. llama3:latest"
631
+ value={config.ollamaHost}
632
+ onChange={(e) => updateField('ollamaHost', e.target.value)}
633
+ placeholder="e.g. http://localhost:11434"
515
634
  />
516
635
  </div>
517
- )}
518
- <div className="setting-row">
519
- <label>Ollama Host</label>
520
- <input
521
- type="text"
522
- value={config.ollamaHost}
523
- onChange={(e) => updateField('ollamaHost', e.target.value)}
524
- placeholder="e.g. http://localhost:11434"
525
- />
526
636
  </div>
527
637
  </div>
528
638
  </div>
529
- </div>
530
- </section>
639
+ </>
640
+ )}
531
641
 
532
- {/* ── Search ── */}
533
- <section className="setting-section">
534
- <div className="section-heading">
535
- <div>
536
- <p className="section-kicker">Web search</p>
537
- <h2 className="section-title">Search</h2>
538
- </div>
539
- <p className="section-description">Choose which search API Mint uses when performing web lookups.</p>
540
- </div>
642
+ {/* ── Section 2: Web Search ── */}
643
+ {renderCollapsibleSection(
644
+ 'search',
645
+ 'Web search',
646
+ 'Search Provider',
647
+ 'Choose which search API Mint uses when performing web lookups.',
541
648
  <div className="provider-cards-container">
542
649
  {/* Brave Search */}
543
650
  <div className={`provider-card ${config.searchProvider === 'brave' ? 'active-provider' : ''}`}>
@@ -609,17 +716,14 @@ export default function GeneralTab({
609
716
  </div>
610
717
  </div>
611
718
  </div>
612
- </section>
719
+ )}
613
720
 
614
- {/* ── Productivity ── */}
615
- <section className="setting-section">
616
- <div className="section-heading">
617
- <div>
618
- <p className="section-kicker">Productivity integrations</p>
619
- <h2 className="section-title">Productivity</h2>
620
- </div>
621
- <p className="section-description">Enable and configure Gmail, Google Calendar, and Notion plugins directly.</p>
622
- </div>
721
+ {/* ── Section 3: Productivity Integrations ── */}
722
+ {renderCollapsibleSection(
723
+ 'productivity',
724
+ 'Productivity integrations',
725
+ 'Productivity',
726
+ 'Enable and configure Gmail, Google Calendar, and Notion plugins directly.',
623
727
  <div className="provider-cards-container">
624
728
  {/* Gmail */}
625
729
  <div className={`provider-card ${config.pluginGmailEnabled ? 'active-provider' : ''}`}>
@@ -764,17 +868,14 @@ export default function GeneralTab({
764
868
  </div>
765
869
  </div>
766
870
  </div>
767
- </section>
871
+ )}
768
872
 
769
- {/* ── Image Generation ── */}
770
- <section className="setting-section">
771
- <div className="section-heading">
772
- <div>
773
- <p className="section-kicker">AI image creation</p>
774
- <h2 className="section-title">Image Generation</h2>
775
- </div>
776
- <p className="section-description">Choose which image generation provider Mint uses.</p>
777
- </div>
873
+ {/* ── Section 4: Image Generation ── */}
874
+ {renderCollapsibleSection(
875
+ 'image_gen',
876
+ 'AI image creation',
877
+ 'Image Generation',
878
+ 'Choose which image generation provider Mint uses.',
778
879
  <div className="provider-cards-container">
779
880
  {/* Gemini Images */}
780
881
  <div className={`provider-card ${config.imageGenProvider === 'gemini' ? 'active-provider' : ''}`}>
@@ -919,225 +1020,260 @@ export default function GeneralTab({
919
1020
  </div>
920
1021
  </div>
921
1022
  </div>
922
- </section>
1023
+ )}
923
1024
 
924
- {/* ─── Custom Providers ─────────────────────────────────────────────────── */}
925
- <section className="setting-section">
926
- <div className="section-heading">
927
- <div>
928
- <p className="section-kicker">Custom AI Routing</p>
929
- <h2 className="section-title">Custom Providers</h2>
1025
+ {/* ── Section 5: Video Generation ── */}
1026
+ {renderCollapsibleSection(
1027
+ 'video_gen',
1028
+ 'AI video creation',
1029
+ 'Video Generation',
1030
+ 'Choose which video generation provider Mint uses.',
1031
+ <div className="provider-cards-container">
1032
+ {/* Veo */}
1033
+ <div className={`provider-card ${config.videoGenProvider === 'veo' ? 'active-provider' : ''}`}>
1034
+ <div className="provider-card-header">
1035
+ <div className="provider-card-title">
1036
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1037
+ <polygon points="23 7 16 12 23 17 23 7" />
1038
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
1039
+ </svg>
1040
+ Google Veo (Gemini Videos)
1041
+ </div>
1042
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1043
+ {config.videoGenProvider === 'veo' && <span className="provider-active-badge">Active</span>}
1044
+ <label className="settings-toggle-switch" style={{ marginBottom: 0 }}>
1045
+ <input type="radio" name="videoGenProvider" style={{ display: 'none' }} checked={config.videoGenProvider === 'veo'} onChange={() => updateField('videoGenProvider', 'veo')} />
1046
+ <span className="settings-toggle-slider" style={{ cursor: 'pointer' }} onClick={() => updateField('videoGenProvider', 'veo')} />
1047
+ </label>
1048
+ </div>
1049
+ </div>
1050
+ <div className="provider-card-body">
1051
+ <p className="hint" style={{ margin: 0 }}>Uses your Gemini API key — no extra key needed.</p>
1052
+ <div className="setting-row" style={{ marginTop: '12px' }}>
1053
+ <label>Default Veo Model</label>
1054
+ <select
1055
+ value={config.veoModel || 'veo-2.0-generate-001'}
1056
+ onChange={(e) => updateField('veoModel', e.target.value)}
1057
+ style={{ width: '100%', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '6px', borderRadius: '4px' }}
1058
+ >
1059
+ <option value="veo-2.0-generate-001">veo-2.0-generate-001 (Default)</option>
1060
+ <option value="veo-2.0-flash-001">veo-2.0-flash-001</option>
1061
+ </select>
1062
+ </div>
1063
+ </div>
930
1064
  </div>
931
- <p className="section-description">
932
- Add any OpenAI-compatible endpoint as a custom provider. Configure its models and optional headers.
933
- </p>
934
1065
  </div>
1066
+ )}
935
1067
 
936
- <div className="provider-cards-container">
937
- {(config.customProviders ?? []).map((cp, cpIdx) => {
938
- const isActive = config.aiProvider === `custom:${cp.id}`
1068
+ {/* ── Section 6: Custom Providers ── */}
1069
+ {renderCollapsibleSection(
1070
+ 'custom_providers',
1071
+ 'Custom AI Routing',
1072
+ 'Custom Providers',
1073
+ 'Add any OpenAI-compatible endpoint as a custom provider. Configure its models and optional headers.',
1074
+ <>
1075
+ <div className="provider-cards-container">
1076
+ {(config.customProviders ?? []).map((cp, cpIdx) => {
1077
+ const isActive = config.aiProvider === `custom:${cp.id}`
939
1078
 
940
- const updateCp = (patch: Partial<CustomProviderConfig>) => {
941
- const updated = (config.customProviders ?? []).map((p, i) =>
942
- i === cpIdx ? { ...p, ...patch } : p
943
- )
944
- updateField('customProviders', updated)
945
- }
1079
+ const updateCp = (patch: Partial<CustomProviderConfig>) => {
1080
+ const updated = (config.customProviders ?? []).map((p, i) =>
1081
+ i === cpIdx ? { ...p, ...patch } : p
1082
+ )
1083
+ updateField('customProviders', updated)
1084
+ }
946
1085
 
947
- const deleteCp = () => {
948
- const updated = (config.customProviders ?? []).filter((_, i) => i !== cpIdx)
949
- updateField('customProviders', updated)
950
- if (config.aiProvider === `custom:${cp.id}`) {
951
- updateField('aiProvider', 'gemini')
1086
+ const deleteCp = () => {
1087
+ const updated = (config.customProviders ?? []).filter((_, i) => i !== cpIdx)
1088
+ updateField('customProviders', updated)
1089
+ if (config.aiProvider === `custom:${cp.id}`) {
1090
+ updateField('aiProvider', 'gemini')
1091
+ }
952
1092
  }
953
- }
954
1093
 
955
- const updateModel = (mIdx: number, patch: Partial<CustomProviderModel>) => {
956
- const updatedModels = cp.models.map((m, i) => i === mIdx ? { ...m, ...patch } : m)
957
- updateCp({ models: updatedModels })
958
- }
959
- const deleteModel = (mIdx: number) => {
960
- updateCp({ models: cp.models.filter((_, i) => i !== mIdx) })
961
- }
962
- const addModel = () => {
963
- updateCp({ models: [...cp.models, { modelId: '', displayName: '' }] })
964
- }
1094
+ const updateModel = (mIdx: number, patch: Partial<CustomProviderModel>) => {
1095
+ const updatedModels = cp.models.map((m, i) => i === mIdx ? { ...m, ...patch } : m)
1096
+ updateCp({ models: updatedModels })
1097
+ }
1098
+ const deleteModel = (mIdx: number) => {
1099
+ updateCp({ models: cp.models.filter((_, i) => i !== mIdx) })
1100
+ }
1101
+ const addModel = () => {
1102
+ updateCp({ models: [...cp.models, { modelId: '', displayName: '' }] })
1103
+ }
965
1104
 
966
- const updateHeader = (hIdx: number, patch: Partial<CustomProviderHeader>) => {
967
- const updatedHeaders = cp.headers.map((h, i) => i === hIdx ? { ...h, ...patch } : h)
968
- updateCp({ headers: updatedHeaders })
969
- }
970
- const deleteHeader = (hIdx: number) => {
971
- updateCp({ headers: cp.headers.filter((_, i) => i !== hIdx) })
972
- }
973
- const addHeader = () => {
974
- updateCp({ headers: [...cp.headers, { name: '', value: '' }] })
975
- }
1105
+ const updateHeader = (hIdx: number, patch: Partial<CustomProviderHeader>) => {
1106
+ const updatedHeaders = cp.headers.map((h, i) => i === hIdx ? { ...h, ...patch } : h)
1107
+ updateCp({ headers: updatedHeaders })
1108
+ }
1109
+ const deleteHeader = (hIdx: number) => {
1110
+ updateCp({ headers: cp.headers.filter((_, i) => i !== hIdx) })
1111
+ }
1112
+ const addHeader = () => {
1113
+ updateCp({ headers: [...cp.headers, { name: '', value: '' }] })
1114
+ }
976
1115
 
977
- return (
978
- <div key={cpIdx} className={`provider-card ${isActive ? 'active-provider' : ''}`}>
979
- <div className="provider-card-header">
980
- <div className="provider-card-title">
981
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
982
- <circle cx="12" cy="12" r="3" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14" />
983
- </svg>
984
- {cp.displayName || cp.id || 'Unnamed Provider'}
985
- </div>
986
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
987
- {isActive && <span className="provider-active-badge">Active</span>}
988
- <button
989
- className="btn btn-primary"
990
- style={{ padding: '3px 8px', fontSize: '12px', background: savedProviderId === cp.id ? '#10b981' : '' }}
991
- onClick={() => handleSaveProvider(cp.id)}
992
- title="Save this provider settings"
993
- >
994
- {savedProviderId === cp.id ? 'Saved! ✓' : 'Save'}
995
- </button>
996
- <button
997
- className="btn btn-danger"
998
- style={{ padding: '3px 8px', fontSize: '12px' }}
999
- onClick={deleteCp}
1000
- title="Delete this provider"
1001
- >
1002
- Delete
1003
- </button>
1116
+ return (
1117
+ <div key={cpIdx} className={`provider-card ${isActive ? 'active-provider' : ''}`}>
1118
+ <div className="provider-card-header">
1119
+ <div className="provider-card-title">
1120
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1121
+ <circle cx="12" cy="12" r="3" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14" />
1122
+ </svg>
1123
+ {cp.displayName || cp.id || 'Unnamed Provider'}
1124
+ </div>
1125
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1126
+ {isActive && <span className="provider-active-badge">Active</span>}
1127
+ <button
1128
+ className="btn btn-primary"
1129
+ style={{ padding: '3px 8px', fontSize: '12px', background: savedProviderId === cp.id ? '#10b981' : '' }}
1130
+ onClick={() => handleSaveProvider(cp.id)}
1131
+ title="Save this provider settings"
1132
+ >
1133
+ {savedProviderId === cp.id ? 'Saved! ✓' : 'Save'}
1134
+ </button>
1135
+ <button
1136
+ className="btn btn-danger"
1137
+ style={{ padding: '3px 8px', fontSize: '12px' }}
1138
+ onClick={deleteCp}
1139
+ title="Delete this provider"
1140
+ >
1141
+ Delete
1142
+ </button>
1143
+ </div>
1004
1144
  </div>
1005
- </div>
1006
1145
 
1007
- <div className="provider-card-body">
1008
- <div className="setting-row">
1009
- <label>Name</label>
1010
- <input
1011
- type="text"
1012
- value={cp.displayName}
1013
- onChange={(e) => {
1014
- const name = e.target.value
1015
- const computedId = name
1016
- .toLowerCase()
1017
- .replace(/[^a-z0-9_-]/g, '-')
1018
- .replace(/-+/g, '-')
1019
- .replace(/^-|-$/g, '') || `provider-${cpIdx + 1}`
1020
- updateCp({ displayName: name, id: computedId })
1021
- }}
1022
- placeholder="e.g. DeepSeek"
1023
- />
1024
- </div>
1146
+ <div className="provider-card-body">
1147
+ <div className="setting-row">
1148
+ <label>Name</label>
1149
+ <input
1150
+ type="text"
1151
+ value={cp.displayName}
1152
+ onChange={(e) => {
1153
+ const name = e.target.value
1154
+ const computedId = name
1155
+ .toLowerCase()
1156
+ .replace(/[^a-z0-9_-]/g, '-')
1157
+ .replace(/-+/g, '-')
1158
+ .replace(/^-|-$/g, '') || `provider-${cpIdx + 1}`
1159
+ updateCp({ displayName: name, id: computedId })
1160
+ }}
1161
+ placeholder="e.g. DeepSeek"
1162
+ />
1163
+ </div>
1025
1164
 
1026
- <div className="setting-row">
1027
- <label>Base URL</label>
1028
- <input
1029
- type="text"
1030
- value={cp.baseUrl}
1031
- onChange={(e) => updateCp({ baseUrl: e.target.value })}
1032
- placeholder="https://api.myprovider.com/v1"
1033
- />
1034
- </div>
1165
+ <div className="setting-row">
1166
+ <label>Base URL</label>
1167
+ <input
1168
+ type="text"
1169
+ value={cp.baseUrl}
1170
+ onChange={(e) => updateCp({ baseUrl: e.target.value })}
1171
+ placeholder="https://api.myprovider.com/v1"
1172
+ />
1173
+ </div>
1035
1174
 
1036
- <div className="setting-row">
1037
- <label>API Key</label>
1038
- <input
1039
- type="password"
1040
- value={cp.apiKey}
1041
- onChange={(e) => updateCp({ apiKey: e.target.value })}
1042
- placeholder="Optional. Leave empty if you manage auth via headers."
1043
- />
1044
- </div>
1175
+ <div className="setting-row">
1176
+ <label>API Key</label>
1177
+ <input
1178
+ type="password"
1179
+ value={cp.apiKey}
1180
+ onChange={(e) => updateCp({ apiKey: e.target.value })}
1181
+ placeholder="Optional. Leave empty if you manage auth via headers."
1182
+ />
1183
+ </div>
1045
1184
 
1046
- {/* Models */}
1047
- <div style={{ marginTop: '12px' }}>
1048
- <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Model list</label>
1049
- {cp.models.map((m, mIdx) => (
1050
- <div key={mIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1051
- <input
1052
- type="text"
1053
- value={m.modelId}
1054
- onChange={(e) => updateModel(mIdx, { modelId: e.target.value, displayName: e.target.value })}
1055
- placeholder="e.g. llama-3.1-8b"
1056
- style={{ flex: 1 }}
1057
- />
1058
- <button
1059
- className="btn btn-danger"
1060
- style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1061
- onClick={() => deleteModel(mIdx)}
1062
- title="Remove model"
1063
- >🗑</button>
1064
- </div>
1065
- ))}
1066
- <button
1067
- className="btn btn-secondary"
1068
- style={{ fontSize: '12px', marginTop: '4px' }}
1069
- onClick={addModel}
1070
- >
1071
- + Add model
1072
- </button>
1073
- </div>
1185
+ <div style={{ marginTop: '12px' }}>
1186
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Model list</label>
1187
+ {cp.models.map((m, mIdx) => (
1188
+ <div key={mIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1189
+ <input
1190
+ type="text"
1191
+ value={m.modelId}
1192
+ onChange={(e) => updateModel(mIdx, { modelId: e.target.value, displayName: e.target.value })}
1193
+ placeholder="e.g. llama-3.1-8b"
1194
+ style={{ flex: 1 }}
1195
+ />
1196
+ <button
1197
+ className="btn btn-danger"
1198
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1199
+ onClick={() => deleteModel(mIdx)}
1200
+ title="Remove model"
1201
+ >🗑</button>
1202
+ </div>
1203
+ ))}
1204
+ <button
1205
+ className="btn btn-secondary"
1206
+ style={{ fontSize: '12px', marginTop: '4px' }}
1207
+ onClick={addModel}
1208
+ >
1209
+ + Add model
1210
+ </button>
1211
+ </div>
1074
1212
 
1075
- {/* Headers */}
1076
- <div style={{ marginTop: '12px' }}>
1077
- <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Headers (optional)</label>
1078
- {cp.headers.map((h, hIdx) => (
1079
- <div key={hIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1080
- <input
1081
- type="text"
1082
- value={h.name}
1083
- onChange={(e) => updateHeader(hIdx, { name: e.target.value })}
1084
- placeholder="Header-Name"
1085
- style={{ flex: 1 }}
1086
- />
1087
- <input
1088
- type="text"
1089
- value={h.value}
1090
- onChange={(e) => updateHeader(hIdx, { value: e.target.value })}
1091
- placeholder="value"
1092
- style={{ flex: 1 }}
1093
- />
1094
- <button
1095
- className="btn btn-danger"
1096
- style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1097
- onClick={() => deleteHeader(hIdx)}
1098
- title="Remove header"
1099
- >🗑</button>
1100
- </div>
1101
- ))}
1102
- <button
1103
- className="btn btn-secondary"
1104
- style={{ fontSize: '12px', marginTop: '4px' }}
1105
- onClick={addHeader}
1106
- >
1107
- + Add header
1108
- </button>
1213
+ <div style={{ marginTop: '12px' }}>
1214
+ <label style={{ fontWeight: 600, fontSize: '13px', display: 'block', marginBottom: '8px' }}>Headers (optional)</label>
1215
+ {cp.headers.map((h, hIdx) => (
1216
+ <div key={hIdx} style={{ display: 'flex', gap: '8px', marginBottom: '6px', alignItems: 'center' }}>
1217
+ <input
1218
+ type="text"
1219
+ value={h.name}
1220
+ onChange={(e) => updateHeader(hIdx, { name: e.target.value })}
1221
+ placeholder="Header-Name"
1222
+ style={{ flex: 1 }}
1223
+ />
1224
+ <input
1225
+ type="text"
1226
+ value={h.value}
1227
+ onChange={(e) => updateHeader(hIdx, { value: e.target.value })}
1228
+ placeholder="value"
1229
+ style={{ flex: 1 }}
1230
+ />
1231
+ <button
1232
+ className="btn btn-danger"
1233
+ style={{ padding: '4px 8px', fontSize: '12px', flexShrink: 0 }}
1234
+ onClick={() => deleteHeader(hIdx)}
1235
+ title="Remove header"
1236
+ >🗑</button>
1237
+ </div>
1238
+ ))}
1239
+ <button
1240
+ className="btn btn-secondary"
1241
+ style={{ fontSize: '12px', marginTop: '4px' }}
1242
+ onClick={addHeader}
1243
+ >
1244
+ + Add header
1245
+ </button>
1246
+ </div>
1109
1247
  </div>
1110
1248
  </div>
1111
- </div>
1112
- )
1113
- })}
1114
- </div>
1115
-
1116
- <div className="setting-actions" style={{ marginTop: '16px' }}>
1117
- <button
1118
- className="btn btn-secondary"
1119
- onClick={() => {
1120
- const newId = `provider-${(config.customProviders ?? []).length + 1}`
1121
- updateField('customProviders', [
1122
- ...(config.customProviders ?? []),
1123
- { id: newId, displayName: '', baseUrl: '', apiKey: '', models: [], headers: [] }
1124
- ])
1125
- }}
1126
- >
1127
- + Add custom provider
1128
- </button>
1129
- </div>
1130
- </section>
1249
+ )
1250
+ })}
1251
+ </div>
1131
1252
 
1132
- {isDesktopApp && (
1133
- <section className="setting-section">
1134
- <div className="section-heading">
1135
- <div>
1136
- <p className="section-kicker">Desktop updates</p>
1137
- <h2 className="section-title">Signed Tauri Channel</h2>
1138
- </div>
1139
- <p className="section-description">Check and explicitly install signed Tauri releases from your configured update channel.</p>
1253
+ <div className="setting-actions" style={{ marginTop: '16px' }}>
1254
+ <button
1255
+ className="btn btn-secondary"
1256
+ onClick={() => {
1257
+ const newId = `provider-${(config.customProviders ?? []).length + 1}`
1258
+ updateField('customProviders', [
1259
+ ...(config.customProviders ?? []),
1260
+ { id: newId, displayName: '', baseUrl: '', apiKey: '', models: [], headers: [] }
1261
+ ])
1262
+ }}
1263
+ >
1264
+ + Add custom provider
1265
+ </button>
1140
1266
  </div>
1267
+ </>
1268
+ )}
1269
+
1270
+ {/* ── Section 7: Desktop Updates ── */}
1271
+ {isDesktopApp && renderCollapsibleSection(
1272
+ 'desktop_updates',
1273
+ 'Desktop updates',
1274
+ 'Signed Tauri Channel',
1275
+ 'Check and explicitly install signed Tauri releases from your configured update channel.',
1276
+ <>
1141
1277
  <div className="toggle-row">
1142
1278
  <div>
1143
1279
  <label>Check signed update channel</label>
@@ -1167,32 +1303,29 @@ export default function GeneralTab({
1167
1303
  {updateAvailable && <button type="button" className="btn-primary" onClick={handleInstallUpdate}>Install signed update</button>}
1168
1304
  </div>
1169
1305
  {updateMessage && <p className="hint">{updateMessage}</p>}
1170
- </section>
1306
+ </>
1171
1307
  )}
1172
1308
 
1173
- {isDesktopApp && (
1174
- <section className="setting-section">
1175
- <div className="section-heading">
1176
- <div>
1177
- <p className="section-kicker">Desktop</p>
1178
- <h2 className="section-title">Assistant Presence</h2>
1179
- </div>
1180
- </div>
1181
- <div className="toggle-row">
1182
- <div>
1183
- <label>Show Desktop AI Candidate</label>
1184
- <p className="hint">Show the mini AI character on your desktop.</p>
1185
- </div>
1186
- <label className="settings-toggle-switch">
1187
- <input
1188
- type="checkbox"
1189
- checked={config.showDesktopWidget}
1190
- onChange={(e) => updateField('showDesktopWidget', e.target.checked)}
1191
- />
1192
- <span className="settings-toggle-slider"></span>
1193
- </label>
1309
+ {/* ── Section 8: Assistant Presence ── */}
1310
+ {isDesktopApp && renderCollapsibleSection(
1311
+ 'assistant_presence',
1312
+ 'Desktop',
1313
+ 'Assistant Presence',
1314
+ 'Configure the mini AI character desktop presence widget.',
1315
+ <div className="toggle-row">
1316
+ <div>
1317
+ <label>Show Desktop AI Candidate</label>
1318
+ <p className="hint">Show the mini AI character on your desktop.</p>
1194
1319
  </div>
1195
- </section>
1320
+ <label className="settings-toggle-switch">
1321
+ <input
1322
+ type="checkbox"
1323
+ checked={config.showDesktopWidget}
1324
+ onChange={(e) => updateField('showDesktopWidget', e.target.checked)}
1325
+ />
1326
+ <span className="settings-toggle-slider"></span>
1327
+ </label>
1328
+ </div>
1196
1329
  )}
1197
1330
  </div>
1198
1331
  )