@pheem49/mint 1.7.1 → 1.8.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 (51) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/crates/mint-core/src/api_server.rs +62 -4
  5. package/crates/mint-core/src/chat.rs +79 -0
  6. package/crates/mint-core/src/lib.rs +1 -1
  7. package/crates/mint-core/src/workflows.rs +8 -0
  8. package/package.json +2 -1
  9. package/src/renderer/src/App.tsx +1 -1
  10. package/src/renderer/src/components/ChatPanel.tsx +27 -14
  11. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  12. package/src/renderer/src/components/ImageStudioPanel.tsx +7 -3
  13. package/src/renderer/src/components/MintDashboard.tsx +199 -5
  14. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  15. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  16. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  17. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  18. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  19. package/src/renderer/src/components/Settings/PluginsTab.tsx +452 -0
  20. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  21. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  22. package/src/renderer/src/components/SettingsWindow.tsx +175 -962
  23. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  24. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  25. package/src/renderer/src/css/image-studio.css +9 -4
  26. package/src/renderer/src/css/settings.css +75 -6
  27. package/src/renderer/src/css/styles.css +355 -38
  28. package/src/renderer/src/css/workflows.css +542 -0
  29. package/src/renderer/src/env.d.ts +2 -0
  30. package/src/renderer/src/index.css +1 -0
  31. package/src/renderer/src/tauri.ts +97 -0
  32. package/src/renderer/src-web/components/ChatPanel.tsx +27 -14
  33. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  34. package/src/renderer/src-web/components/ImageStudioPanel.tsx +7 -3
  35. package/src/renderer/src-web/components/MintDashboard.tsx +189 -0
  36. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  37. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  38. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  39. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  40. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  41. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +452 -0
  42. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  43. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  44. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -968
  45. package/src/renderer/src-web/css/image-studio.css +9 -4
  46. package/src/renderer/src-web/css/settings.css +75 -6
  47. package/src/renderer/src-web/css/styles.css +294 -38
  48. package/src/renderer/src-web/env.d.ts +2 -0
  49. package/src/renderer/src-web/tauri.ts +36 -0
  50. package/src-tauri/src/lib.rs +83 -90
  51. package/src-tauri/tauri.conf.json +1 -1
@@ -5,6 +5,7 @@ import {
5
5
  deleteChatSession,
6
6
  renameChatSession,
7
7
  getRecentInteractions,
8
+ saveSystemInteraction,
8
9
  getRuntimeStatus,
9
10
  listChatSessions,
10
11
  listSavedPictures,
@@ -28,6 +29,7 @@ import ModelPanel from './ModelPanel'
28
29
  import type { ModelInteraction } from './ModelPanel'
29
30
  import PicturesLibrary from './PicturesLibrary'
30
31
  import WorkspacePanel from './WorkspacePanel'
32
+ import WorkflowBuilderPanel from './WorkflowBuilderPanel'
31
33
 
32
34
  const EXPRESSIONS = [
33
35
  "Default",
@@ -267,6 +269,79 @@ export default function MintDashboard() {
267
269
  const chatEnd = useRef<HTMLDivElement | null>(null)
268
270
  const startupReady = (dashboardDataReady && modelReady) || startupTimedOut
269
271
 
272
+ const [isSearchOpen, setIsSearchOpen] = useState(false)
273
+ const [searchQuery, setSearchQuery] = useState('')
274
+
275
+ useEffect(() => {
276
+ function handleKeyDown(event: KeyboardEvent) {
277
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
278
+ event.preventDefault()
279
+ setIsSearchOpen((prev) => !prev)
280
+ } else if (event.key === 'Escape') {
281
+ setIsSearchOpen(false)
282
+ }
283
+ }
284
+ window.addEventListener('keydown', handleKeyDown)
285
+ return () => window.removeEventListener('keydown', handleKeyDown)
286
+ }, [])
287
+
288
+ useEffect(() => {
289
+ if (!isSearchOpen) {
290
+ setSearchQuery('')
291
+ }
292
+ }, [isSearchOpen])
293
+
294
+ const filteredSessions = chatSessions.filter((session) => {
295
+ if (session.kind === 'cli' || session.id === 'conversation-default') return false
296
+ return session.title.toLowerCase().includes(searchQuery.toLowerCase())
297
+ })
298
+
299
+ const groupSessionsByDate = (sessions: ChatSession[]) => {
300
+ const groups: { [key: string]: ChatSession[] } = {}
301
+
302
+ sessions.forEach((session) => {
303
+ const dateStr = session.updatedAt || session.createdAt
304
+ if (!dateStr) {
305
+ const groupName = 'Other'
306
+ if (!groups[groupName]) groups[groupName] = []
307
+ groups[groupName].push(session)
308
+ return
309
+ }
310
+
311
+ const date = new Date(dateStr)
312
+ const today = new Date()
313
+ const yesterday = new Date()
314
+ yesterday.setDate(today.getDate() - 1)
315
+
316
+ let groupName = ''
317
+ if (date.toDateString() === today.toDateString()) {
318
+ groupName = 'Today'
319
+ } else if (date.toDateString() === yesterday.toDateString()) {
320
+ groupName = 'Yesterday'
321
+ } else {
322
+ const diffTime = Math.abs(today.getTime() - date.getTime())
323
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
324
+ if (diffDays <= 7) {
325
+ groupName = 'Previous 7 days'
326
+ } else if (diffDays <= 30) {
327
+ groupName = 'Previous 30 days'
328
+ } else {
329
+ groupName = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
330
+ }
331
+ }
332
+
333
+ if (!groups[groupName]) {
334
+ groups[groupName] = []
335
+ }
336
+ groups[groupName].push(session)
337
+ })
338
+
339
+ return groups
340
+ }
341
+
342
+ const groupedSearchSessions = groupSessionsByDate(filteredSessions)
343
+
344
+
270
345
  async function refreshHistory() {
271
346
  const history = await getRecentInteractions(50, conversationId)
272
347
  const reversed = history.reverse()
@@ -361,7 +436,7 @@ export default function MintDashboard() {
361
436
  }, [view])
362
437
 
363
438
  useEffect(() => {
364
- if (view === 'workspeac' && !agentMode) updateAgentMode(true)
439
+ if (view === 'workspace' && !agentMode) updateAgentMode(true)
365
440
  }, [view, agentMode])
366
441
 
367
442
  useEffect(() => {
@@ -674,7 +749,7 @@ export default function MintDashboard() {
674
749
  const selected = await selectWorkspaceDirectory()
675
750
  if (selected) {
676
751
  updateWorkspacePath(selected)
677
- setView('workspeac')
752
+ setView('workspace')
678
753
  }
679
754
  } catch (reason) {
680
755
  setError(errorMessage(reason))
@@ -781,6 +856,37 @@ export default function MintDashboard() {
781
856
  }
782
857
  }
783
858
 
859
+ function formatProviderChangeText(provider: string, model: string) {
860
+ let providerName = provider
861
+ if (provider === 'gemini') providerName = 'Gemini'
862
+ else if (provider === 'openai') providerName = 'OpenAI'
863
+ else if (provider === 'openrouter') providerName = 'OpenRouter'
864
+ else if (provider === 'deepseek') providerName = 'DeepSeek'
865
+ else if (provider === 'anthropic') providerName = 'Claude'
866
+ else if (provider === 'huggingface') providerName = 'HF'
867
+ else if (provider === 'local_openai') providerName = 'Local'
868
+ else if (provider === 'ollama') providerName = 'Ollama'
869
+
870
+ if (providerName && providerName === provider) {
871
+ providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
872
+ }
873
+ return [providerName, model].filter(Boolean).join(' • ')
874
+ }
875
+
876
+ function getActiveModelName(config: any, provider: string) {
877
+ switch (provider) {
878
+ case 'gemini': return config.geminiModel || 'gemini-1.5-flash'
879
+ case 'openai': return config.openaiModel || 'gpt-4o'
880
+ case 'openrouter': return config.openrouterModel || 'anthropic/claude-3.5-sonnet'
881
+ case 'deepseek': return config.deepseekModel || 'deepseek-chat'
882
+ case 'anthropic': return config.anthropicModel || 'claude-3-5-sonnet-20240620'
883
+ case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
884
+ case 'local_openai': return config.localModelName || 'llama3'
885
+ case 'ollama': return config.ollamaModel || 'llama3:latest'
886
+ default: return ''
887
+ }
888
+ }
889
+
784
890
  async function changeProvider(provider: string) {
785
891
  try {
786
892
  const config = await window.settingsApi.getSettings()
@@ -788,6 +894,12 @@ export default function MintDashboard() {
788
894
  await window.settingsApi.saveSettings(config)
789
895
  setSettingsConfig(config)
790
896
  setStatus(await getRuntimeStatus())
897
+
898
+ // Record system event in chat history
899
+ const activeModel = getActiveModelName(config, provider)
900
+ const displayName = formatProviderChangeText(provider, activeModel)
901
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
902
+ await refreshHistory()
791
903
  } catch (reason) {
792
904
  setError(errorMessage(reason))
793
905
  }
@@ -817,6 +929,11 @@ export default function MintDashboard() {
817
929
  await window.settingsApi.saveSettings(config)
818
930
  setSettingsConfig(config)
819
931
  setStatus(await getRuntimeStatus())
932
+
933
+ // Record system event in chat history
934
+ const displayName = formatProviderChangeText(provider, modelName)
935
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
936
+ await refreshHistory()
820
937
  } catch (reason) {
821
938
  setError(errorMessage(reason))
822
939
  }
@@ -897,9 +1014,11 @@ export default function MintDashboard() {
897
1014
  onSetInteractionEnabled={updateInteractionEnabled}
898
1015
  onSetShowInteractionGuide={updateInteractionGuide}
899
1016
  onShowToast={showToast}
1017
+ isSearchOpen={isSearchOpen}
1018
+ onSetSearchOpen={setIsSearchOpen}
900
1019
  />
901
- <main className={`assistant-workspace ${layoutPreset === 'chat-wide' ? 'layout-chat-wide' : 'layout-model-wide'} ${modelVisible || view === 'workspeac' ? '' : 'model-hidden'} ${view === 'workspeac' ? 'workspace-open' : ''}`}>
902
- {view === 'workspeac' && (
1020
+ <main className={`assistant-workspace ${layoutPreset === 'chat-wide' ? 'layout-chat-wide' : 'layout-model-wide'} ${modelVisible || view === 'workspace' ? '' : 'model-hidden'} ${view === 'workspace' ? 'workspace-open' : ''}`}>
1021
+ {view === 'workspace' && (
903
1022
  <WorkspacePanel
904
1023
  agentMode={agentMode}
905
1024
  sending={sending}
@@ -914,7 +1033,7 @@ export default function MintDashboard() {
914
1033
  expressionIndex={expressionIndex}
915
1034
  accessoryIndex={accessoryIndex}
916
1035
  isLocked={isLocked}
917
- isActive={modelVisible && view !== 'pictures' && view !== 'workspeac'}
1036
+ isActive={modelVisible && view !== 'pictures' && view !== 'workspace' && view !== 'workflows'}
918
1037
  layoutPreset={layoutPreset}
919
1038
  sending={sending}
920
1039
  interactionEnabled={interactionEnabled}
@@ -980,6 +1099,10 @@ export default function MintDashboard() {
980
1099
  setMessage(imgPrompt)
981
1100
  }}
982
1101
  />
1102
+ <WorkflowBuilderPanel
1103
+ view={view}
1104
+ onShowToast={showToast}
1105
+ />
983
1106
  </div>
984
1107
  <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
985
1108
  <div className="startup-loading-content">
@@ -998,6 +1121,77 @@ export default function MintDashboard() {
998
1121
  </button>
999
1122
  </div>
1000
1123
  )}
1124
+
1125
+ {isSearchOpen && (
1126
+ <div className="sidebar-search-modal-backdrop" onClick={() => setIsSearchOpen(false)}>
1127
+ <div className="sidebar-search-modal" onClick={(e) => e.stopPropagation()}>
1128
+ <div className="search-modal-header">
1129
+ <span className="search-icon-wrapper">
1130
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1131
+ <circle cx="11" cy="11" r="8"></circle>
1132
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
1133
+ </svg>
1134
+ </span>
1135
+ <input
1136
+ type="text"
1137
+ placeholder="Search chats..."
1138
+ value={searchQuery}
1139
+ onChange={(e) => setSearchQuery(e.target.value)}
1140
+ autoFocus
1141
+ />
1142
+ <button className="search-modal-close" onClick={() => setIsSearchOpen(false)}>
1143
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1144
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1145
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1146
+ </svg>
1147
+ </button>
1148
+ </div>
1149
+
1150
+ <div className="search-modal-body">
1151
+ <button
1152
+ className="search-new-chat-btn"
1153
+ onClick={() => {
1154
+ clearHistory('New chat')
1155
+ setIsSearchOpen(false)
1156
+ }}
1157
+ >
1158
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1159
+ <line x1="12" y1="5" x2="12" y2="19"></line>
1160
+ <line x1="5" y1="12" x2="19" y2="12"></line>
1161
+ </svg>
1162
+ <span>New Chat</span>
1163
+ </button>
1164
+
1165
+ <div className="search-modal-results">
1166
+ {Object.keys(groupedSearchSessions).length > 0 ? (
1167
+ Object.entries(groupedSearchSessions).map(([groupName, sessions]) => (
1168
+ <div key={groupName} className="search-results-group">
1169
+ <div className="search-group-title">{groupName}</div>
1170
+ {sessions.map((session) => (
1171
+ <button
1172
+ key={session.id}
1173
+ className={`search-result-item ${session.id === conversationId ? 'active' : ''}`}
1174
+ onClick={() => {
1175
+ selectConversation(session.id)
1176
+ setIsSearchOpen(false)
1177
+ }}
1178
+ >
1179
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1180
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
1181
+ </svg>
1182
+ <span className="search-result-title">{session.title || 'New chat'}</span>
1183
+ </button>
1184
+ ))}
1185
+ </div>
1186
+ ))
1187
+ ) : (
1188
+ <div className="search-no-results">No matching chats found</div>
1189
+ )}
1190
+ </div>
1191
+ </div>
1192
+ </div>
1193
+ </div>
1194
+ )}
1001
1195
  </div>
1002
1196
  )
1003
1197
  }
@@ -9,10 +9,8 @@ interface PicturesLibraryProps {
9
9
  }
10
10
 
11
11
  export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPictures }: PicturesLibraryProps) {
12
- if (view !== 'pictures') return null
13
-
14
12
  return (
15
- <section className="pictures-library">
13
+ <section className={`pictures-library ${view === 'pictures' ? 'is-visible' : ''}`} aria-hidden={view !== 'pictures'}>
16
14
  <header className="pictures-header">
17
15
  <div><span className="pictures-kicker">Gallery</span><h2>Saved Pictures</h2></div>
18
16
  <div className="pictures-header-actions">
@@ -0,0 +1,97 @@
1
+ import React from 'react'
2
+ import { DEFAULT_CONFIG } from '../SettingsWindow'
3
+
4
+ interface AudioTabProps {
5
+ config: typeof DEFAULT_CONFIG
6
+ updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
7
+ }
8
+
9
+ export default function AudioTab({ config, updateField }: AudioTabProps) {
10
+ return (
11
+ <div className="tab-pane active">
12
+ <section className="setting-section">
13
+ <div className="section-heading">
14
+ <div>
15
+ <p className="section-kicker">Speech</p>
16
+ <h2 className="section-title">Voice Reply</h2>
17
+ </div>
18
+ <p className="section-description">Control spoken responses and TTS behavior.</p>
19
+ </div>
20
+
21
+ <div className="toggle-row">
22
+ <div>
23
+ <label>Enable Voice Reply</label>
24
+ <p className="hint">Mint will speak responses out loud when this is enabled.</p>
25
+ </div>
26
+ <label className="settings-toggle-switch">
27
+ <input
28
+ type="checkbox"
29
+ checked={config.enableVoiceReply}
30
+ onChange={(e) => updateField('enableVoiceReply', e.target.checked)}
31
+ />
32
+ <span className="settings-toggle-slider"></span>
33
+ </label>
34
+ </div>
35
+
36
+ <div className="form-grid single">
37
+ <div className="setting-row">
38
+ <label>Voice Engine</label>
39
+ <select value={config.ttsProvider} onChange={(e) => updateField('ttsProvider', e.target.value)}>
40
+ <option value="google">Google Cloud (Natural, Auto Lang)</option>
41
+ <option value="native">OS Native (Supports Pitch)</option>
42
+ </select>
43
+ </div>
44
+ </div>
45
+
46
+ <div className="slider-stack">
47
+ <div className="setting-row">
48
+ <label>Volume</label>
49
+ <div className="slider-group">
50
+ <input
51
+ type="range"
52
+ min="0"
53
+ max="1"
54
+ step="0.1"
55
+ value={config.ttsVolume}
56
+ onChange={(e) => updateField('ttsVolume', parseFloat(e.target.value))}
57
+ className="range-slider"
58
+ />
59
+ <span className="range-value">{Math.round(config.ttsVolume * 100)}%</span>
60
+ </div>
61
+ </div>
62
+ <div className="setting-row">
63
+ <label>Speed</label>
64
+ <div className="slider-group">
65
+ <input
66
+ type="range"
67
+ min="0.5"
68
+ max="2"
69
+ step="0.1"
70
+ value={config.ttsSpeed}
71
+ onChange={(e) => updateField('ttsSpeed', parseFloat(e.target.value))}
72
+ className="range-slider"
73
+ />
74
+ <span className="range-value">{parseFloat(String(config.ttsSpeed)).toFixed(1)}x</span>
75
+ </div>
76
+ </div>
77
+ <div className="setting-row">
78
+ <label>Pitch</label>
79
+ <div className="slider-group">
80
+ <input
81
+ type="range"
82
+ min="0"
83
+ max="2"
84
+ step="0.1"
85
+ value={config.ttsPitch}
86
+ onChange={(e) => updateField('ttsPitch', parseFloat(e.target.value))}
87
+ className="range-slider"
88
+ />
89
+ <span className="range-value">{parseFloat(String(config.ttsPitch)).toFixed(1)}</span>
90
+ </div>
91
+ <p className="hint">Pitch applies to OS native voice only.</p>
92
+ </div>
93
+ </div>
94
+ </section>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,195 @@
1
+ import React from 'react'
2
+ import { DEFAULT_CONFIG } from '../SettingsWindow'
3
+
4
+ interface AutomationTabProps {
5
+ config: typeof DEFAULT_CONFIG
6
+ updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
7
+ handleOpenWorkflows: () => void
8
+ handleReloadWorkflows: () => void
9
+ isDesktopApp?: boolean
10
+ }
11
+
12
+ export default function AutomationTab({
13
+ config,
14
+ updateField,
15
+ handleOpenWorkflows,
16
+ handleReloadWorkflows,
17
+ isDesktopApp = true
18
+ }: AutomationTabProps) {
19
+ return (
20
+ <div className="tab-pane active">
21
+ <section className="setting-section">
22
+ <div className="section-heading">
23
+ <div>
24
+ <p className="section-kicker">Browser</p>
25
+ <h2 className="section-title">Automation Engine</h2>
26
+ </div>
27
+ </div>
28
+ <div className="form-grid single">
29
+ <div className="setting-row">
30
+ <label>Browser Engine</label>
31
+ <select value={config.automationBrowser} onChange={(e) => updateField('automationBrowser', e.target.value)}>
32
+ <option value="chromium">Chromium (Bundled)</option>
33
+ <option value="/usr/bin/firefox">Firefox (System - Linux)</option>
34
+ </select>
35
+ </div>
36
+ <div className="setting-row">
37
+ <label>Chromium DevTools Endpoint</label>
38
+ <input type="text" value={config.browserDebugUrl} onChange={(e) => updateField('browserDebugUrl', e.target.value)} />
39
+ <p className="hint">Required for native tab reading and selector clicks.</p>
40
+ </div>
41
+ <div className="setting-row">
42
+ <label>Browser Extension Context Endpoint</label>
43
+ <input type="text" value={config.browserExtensionContextUrl} onChange={(e) => updateField('browserExtensionContextUrl', e.target.value)} />
44
+ <p className="hint">Fallback endpoint used when Chromium remote debugging is unavailable.</p>
45
+ </div>
46
+ </div>
47
+ </section>
48
+
49
+ <section className="setting-section">
50
+ <div className="section-heading">
51
+ <div>
52
+ <p className="section-kicker">Background tasks</p>
53
+ <h2 className="section-title">Native Headless Queue</h2>
54
+ </div>
55
+ </div>
56
+ <div className="toggle-row">
57
+ <div>
58
+ <label>Process queued tasks automatically</label>
59
+ <p className="hint">Allow the bounded Rust worker to process pending tasks every 15 seconds.</p>
60
+ </div>
61
+ <label className="settings-toggle-switch">
62
+ <input
63
+ type="checkbox"
64
+ checked={config.enableHeadlessTaskQueue}
65
+ onChange={(e) => updateField('enableHeadlessTaskQueue', e.target.checked)}
66
+ />
67
+ <span className="settings-toggle-slider"></span>
68
+ </label>
69
+ </div>
70
+ </section>
71
+
72
+ <section className="setting-section">
73
+ <div className="section-heading">
74
+ <div>
75
+ <p className="section-kicker">Awareness</p>
76
+ <h2 className="section-title">Proactive Assistant</h2>
77
+ </div>
78
+ <p className="section-description">Tune screen analysis frequency and suggestion timing.</p>
79
+ </div>
80
+ <div className="slider-stack">
81
+ <div className="setting-row">
82
+ <label>Screen Capture Frequency</label>
83
+ <div className="slider-group">
84
+ <input
85
+ type="range"
86
+ min="30"
87
+ max="300"
88
+ step="30"
89
+ value={config.proactiveInterval}
90
+ onChange={(e) => updateField('proactiveInterval', parseInt(e.target.value))}
91
+ className="range-slider"
92
+ />
93
+ <span className="range-value">{config.proactiveInterval} sec</span>
94
+ </div>
95
+ <p className="hint">Lower values respond faster but use more API calls.</p>
96
+ </div>
97
+ <div className="setting-row">
98
+ <label>Suggestion Cooldown</label>
99
+ <div className="slider-group">
100
+ <input
101
+ type="range"
102
+ min="60"
103
+ max="600"
104
+ step="60"
105
+ value={config.proactiveCooldown}
106
+ onChange={(e) => updateField('proactiveCooldown', parseInt(e.target.value))}
107
+ className="range-slider"
108
+ />
109
+ <span className="range-value">{Math.round(config.proactiveCooldown / 60)} min</span>
110
+ </div>
111
+ <p className="hint">Minimum time between repeat suggestions.</p>
112
+ </div>
113
+ </div>
114
+ </section>
115
+
116
+ <section className="setting-section">
117
+ <div className="section-heading">
118
+ <div>
119
+ <p className="section-kicker">Messaging</p>
120
+ <h2 className="section-title">Native Channel Bridges</h2>
121
+ </div>
122
+ <p className="section-description">Configure credentials used directly by the Rust channel workers.</p>
123
+ </div>
124
+ <div className="form-grid">
125
+ <label className="toggle-row"><span>Telegram Bot API</span><input type="checkbox" checked={config.enableTelegramBridge} onChange={(e) => updateField('enableTelegramBridge', e.target.checked)} /></label>
126
+ <input type="password" placeholder="Telegram bot token" value={config.telegramBotToken} onChange={(e) => updateField('telegramBotToken', e.target.value)} />
127
+ <label className="toggle-row"><span>Discord Gateway</span><input type="checkbox" checked={config.enableDiscordBridge} onChange={(e) => updateField('enableDiscordBridge', e.target.checked)} /></label>
128
+ <input type="password" placeholder="Discord bot token" value={config.discordBotToken} onChange={(e) => updateField('discordBotToken', e.target.value)} />
129
+ <input type="text" placeholder="Discord application ID for Rich Presence" value={config.discordApplicationId} onChange={(e) => updateField('discordApplicationId', e.target.value)} />
130
+ <label className="toggle-row"><span>Slack Socket Mode</span><input type="checkbox" checked={config.enableSlackBridge} onChange={(e) => updateField('enableSlackBridge', e.target.checked)} /></label>
131
+ <input type="password" placeholder="Slack bot token (xoxb-...)" value={config.slackBotToken} onChange={(e) => updateField('slackBotToken', e.target.value)} />
132
+ <input type="password" placeholder="Slack app token (xapp-...)" value={config.slackAppToken} onChange={(e) => updateField('slackAppToken', e.target.value)} />
133
+ <label className="toggle-row"><span>LINE Webhook</span><input type="checkbox" checked={config.enableLineBridge} onChange={(e) => updateField('enableLineBridge', e.target.checked)} /></label>
134
+ <input type="password" placeholder="LINE channel access token" value={config.lineChannelAccessToken} onChange={(e) => updateField('lineChannelAccessToken', e.target.value)} />
135
+ <input type="password" placeholder="LINE channel secret" value={config.lineChannelSecret} onChange={(e) => updateField('lineChannelSecret', e.target.value)} />
136
+ <label className="toggle-row"><span>WhatsApp Cloud API</span><input type="checkbox" checked={config.enableWhatsappBridge} onChange={(e) => updateField('enableWhatsappBridge', e.target.checked)} /></label>
137
+ <input type="password" placeholder="WhatsApp Cloud access token" value={config.whatsappCloudAccessToken} onChange={(e) => updateField('whatsappCloudAccessToken', e.target.value)} />
138
+ <input type="text" placeholder="WhatsApp phone number ID" value={config.whatsappPhoneNumberId} onChange={(e) => updateField('whatsappPhoneNumberId', e.target.value)} />
139
+ <input type="password" placeholder="WhatsApp verify token" value={config.whatsappVerifyToken} onChange={(e) => updateField('whatsappVerifyToken', e.target.value)} />
140
+ <input type="password" placeholder="WhatsApp app secret (optional HMAC validation)" value={config.whatsappAppSecret} onChange={(e) => updateField('whatsappAppSecret', e.target.value)} />
141
+ </div>
142
+ </section>
143
+
144
+ <section className="setting-section">
145
+ <div className="section-heading">
146
+ <div>
147
+ <p className="section-kicker">Rules</p>
148
+ <h2 className="section-title">Custom Workflows</h2>
149
+ </div>
150
+ </div>
151
+ <div className="toggle-row">
152
+ <div>
153
+ <label>Enable Custom Workflows</label>
154
+ <p className="hint">Run "If This Then Mint" rules from the workflow JSON file.</p>
155
+ </div>
156
+ <label className="settings-toggle-switch">
157
+ <input
158
+ type="checkbox"
159
+ checked={config.enableCustomWorkflows}
160
+ onChange={(e) => updateField('enableCustomWorkflows', e.target.checked)}
161
+ />
162
+ <span className="settings-toggle-slider"></span>
163
+ </label>
164
+ </div>
165
+ <div className="button-row" style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
166
+ <button className="btn btn-secondary" onClick={handleOpenWorkflows}>Open workflows.json</button>
167
+ <button className="btn btn-primary" onClick={handleReloadWorkflows}>Reload Rules</button>
168
+ </div>
169
+ </section>
170
+
171
+ <section className="setting-section">
172
+ <div className="section-heading">
173
+ <div>
174
+ <p className="section-kicker">Code mode</p>
175
+ <h2 className="section-title">Agent Collaboration</h2>
176
+ </div>
177
+ </div>
178
+ <div className="toggle-row">
179
+ <div>
180
+ <label>Enable Multi-Agent Review</label>
181
+ <p className="hint">Allow a secondary model to review code written by the primary model.</p>
182
+ </div>
183
+ <label className="settings-toggle-switch">
184
+ <input
185
+ type="checkbox"
186
+ checked={config.enableAgentCollaboration}
187
+ onChange={(e) => updateField('enableAgentCollaboration', e.target.checked)}
188
+ />
189
+ <span className="settings-toggle-slider"></span>
190
+ </label>
191
+ </div>
192
+ </section>
193
+ </div>
194
+ )
195
+ }