@pheem49/mint 1.7.1 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +40 -0
  5. package/Release_Note.md +95 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/crates/mint-cli/Cargo.toml +1 -0
  8. package/crates/mint-cli/src/agent.rs +142 -48
  9. package/crates/mint-cli/src/main.rs +231 -22
  10. package/crates/mint-core/src/api_server.rs +71 -4
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/chat.rs +79 -0
  13. package/crates/mint-core/src/config.rs +69 -1
  14. package/crates/mint-core/src/lib.rs +6 -1
  15. package/crates/mint-core/src/orchestration.rs +137 -1
  16. package/crates/mint-core/src/skills.rs +113 -4
  17. package/crates/mint-core/src/workflows.rs +9 -22
  18. package/package.json +2 -1
  19. package/src/renderer/src/App.tsx +1 -1
  20. package/src/renderer/src/components/ChatPanel.tsx +197 -47
  21. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  22. package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
  23. package/src/renderer/src/components/MintDashboard.tsx +239 -7
  24. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  25. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  26. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  27. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  28. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  29. package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
  30. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  31. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  32. package/src/renderer/src/components/SettingsWindow.tsx +175 -970
  33. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  34. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  35. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  36. package/src/renderer/src/css/image-studio.css +15 -4
  37. package/src/renderer/src/css/settings.css +75 -6
  38. package/src/renderer/src/css/styles.css +419 -49
  39. package/src/renderer/src/css/workflows.css +542 -0
  40. package/src/renderer/src/env.d.ts +2 -0
  41. package/src/renderer/src/index.css +13 -1
  42. package/src/renderer/src/tauri.ts +140 -2
  43. package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
  44. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  45. package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
  46. package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
  47. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  48. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  49. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  50. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  51. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  52. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
  53. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  54. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  55. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
  56. package/src/renderer/src-web/css/image-studio.css +15 -4
  57. package/src/renderer/src-web/css/settings.css +75 -6
  58. package/src/renderer/src-web/css/styles.css +394 -50
  59. package/src/renderer/src-web/env.d.ts +2 -0
  60. package/src/renderer/src-web/index.css +12 -1
  61. package/src/renderer/src-web/tauri.ts +60 -2
  62. package/src-tauri/src/browser.rs +1 -141
  63. package/src-tauri/src/headless.rs +3 -2
  64. package/src-tauri/src/lib.rs +195 -111
  65. package/src-tauri/src/workflows.rs +56 -4
  66. package/src-tauri/tauri.conf.json +1 -1
@@ -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,
@@ -235,6 +236,79 @@ export default function MintDashboard() {
235
236
  const chatEnd = useRef<HTMLDivElement | null>(null)
236
237
  const startupReady = dashboardDataReady || startupTimedOut
237
238
 
239
+ const [isSearchOpen, setIsSearchOpen] = useState(false)
240
+ const [searchQuery, setSearchQuery] = useState('')
241
+
242
+ useEffect(() => {
243
+ function handleKeyDown(event: KeyboardEvent) {
244
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
245
+ event.preventDefault()
246
+ setIsSearchOpen((prev) => !prev)
247
+ } else if (event.key === 'Escape') {
248
+ setIsSearchOpen(false)
249
+ }
250
+ }
251
+ window.addEventListener('keydown', handleKeyDown)
252
+ return () => window.removeEventListener('keydown', handleKeyDown)
253
+ }, [])
254
+
255
+ useEffect(() => {
256
+ if (!isSearchOpen) {
257
+ setSearchQuery('')
258
+ }
259
+ }, [isSearchOpen])
260
+
261
+ const filteredSessions = chatSessions.filter((session) => {
262
+ if (session.kind === 'cli' || session.id === 'conversation-default') return false
263
+ return session.title.toLowerCase().includes(searchQuery.toLowerCase())
264
+ })
265
+
266
+ const groupSessionsByDate = (sessions: ChatSession[]) => {
267
+ const groups: { [key: string]: ChatSession[] } = {}
268
+
269
+ sessions.forEach((session) => {
270
+ const dateStr = session.updatedAt || session.createdAt
271
+ if (!dateStr) {
272
+ const groupName = 'Other'
273
+ if (!groups[groupName]) groups[groupName] = []
274
+ groups[groupName].push(session)
275
+ return
276
+ }
277
+
278
+ const date = new Date(dateStr)
279
+ const today = new Date()
280
+ const yesterday = new Date()
281
+ yesterday.setDate(today.getDate() - 1)
282
+
283
+ let groupName = ''
284
+ if (date.toDateString() === today.toDateString()) {
285
+ groupName = 'Today'
286
+ } else if (date.toDateString() === yesterday.toDateString()) {
287
+ groupName = 'Yesterday'
288
+ } else {
289
+ const diffTime = Math.abs(today.getTime() - date.getTime())
290
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
291
+ if (diffDays <= 7) {
292
+ groupName = 'Previous 7 days'
293
+ } else if (diffDays <= 30) {
294
+ groupName = 'Previous 30 days'
295
+ } else {
296
+ groupName = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
297
+ }
298
+ }
299
+
300
+ if (!groups[groupName]) {
301
+ groups[groupName] = []
302
+ }
303
+ groups[groupName].push(session)
304
+ })
305
+
306
+ return groups
307
+ }
308
+
309
+ const groupedSearchSessions = groupSessionsByDate(filteredSessions)
310
+
311
+
238
312
  async function refreshHistory() {
239
313
  const history = await getRecentInteractions(50, conversationId)
240
314
  const reversed = history.reverse()
@@ -310,6 +384,10 @@ export default function MintDashboard() {
310
384
  }
311
385
 
312
386
  const toggleSidebar = () => {
387
+ if (window.innerWidth <= 760) {
388
+ setMobileSidebarOpen(false)
389
+ return
390
+ }
313
391
  const next = !sidebarCollapsed
314
392
  window.localStorage.setItem('mint:sidebar-collapsed', String(next))
315
393
  setSidebarCollapsed(next)
@@ -326,10 +404,10 @@ export default function MintDashboard() {
326
404
  setAgentMode(enabled)
327
405
  }
328
406
 
329
- async function handleApproval(approved: boolean, _autoApproveSession = false) {
407
+ async function handleApproval(approved: boolean, _autoApproveSession = false, answer?: string) {
330
408
  if (!pendingApproval) return
331
409
  try {
332
- await submitToolApproval(pendingApproval.token, approved)
410
+ await submitToolApproval(pendingApproval.token, approved, answer)
333
411
  } catch (reason) {
334
412
  setError(errorMessage(reason))
335
413
  } finally {
@@ -664,6 +742,37 @@ export default function MintDashboard() {
664
742
  }
665
743
  }
666
744
 
745
+ function formatProviderChangeText(provider: string, model: string) {
746
+ let providerName = provider
747
+ if (provider === 'gemini') providerName = 'Gemini'
748
+ else if (provider === 'openai') providerName = 'OpenAI'
749
+ else if (provider === 'openrouter') providerName = 'OpenRouter'
750
+ else if (provider === 'deepseek') providerName = 'DeepSeek'
751
+ else if (provider === 'anthropic') providerName = 'Claude'
752
+ else if (provider === 'huggingface') providerName = 'HF'
753
+ else if (provider === 'local_openai') providerName = 'Local'
754
+ else if (provider === 'ollama') providerName = 'Ollama'
755
+
756
+ if (providerName && providerName === provider) {
757
+ providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
758
+ }
759
+ return [providerName, model].filter(Boolean).join(' • ')
760
+ }
761
+
762
+ function getActiveModelName(config: any, provider: string) {
763
+ switch (provider) {
764
+ case 'gemini': return config.geminiModel || 'gemini-1.5-flash'
765
+ case 'openai': return config.openaiModel || 'gpt-4o'
766
+ case 'openrouter': return config.openrouterModel || 'anthropic/claude-3.5-sonnet'
767
+ case 'deepseek': return config.deepseekModel || 'deepseek-chat'
768
+ case 'anthropic': return config.anthropicModel || 'claude-3-5-sonnet-20240620'
769
+ case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
770
+ case 'local_openai': return config.localModelName || 'llama3'
771
+ case 'ollama': return config.ollamaModel || 'llama3:latest'
772
+ default: return ''
773
+ }
774
+ }
775
+
667
776
  async function changeProvider(provider: string) {
668
777
  try {
669
778
  const config = await window.settingsApi.getSettings()
@@ -671,6 +780,12 @@ export default function MintDashboard() {
671
780
  await window.settingsApi.saveSettings(config)
672
781
  setSettingsConfig(config)
673
782
  setStatus(await getRuntimeStatus())
783
+
784
+ // Record system event in chat history
785
+ const activeModel = getActiveModelName(config, provider)
786
+ const displayName = formatProviderChangeText(provider, activeModel)
787
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
788
+ await refreshHistory()
674
789
  } catch (reason) {
675
790
  setError(errorMessage(reason))
676
791
  }
@@ -700,6 +815,11 @@ export default function MintDashboard() {
700
815
  await window.settingsApi.saveSettings(config)
701
816
  setSettingsConfig(config)
702
817
  setStatus(await getRuntimeStatus())
818
+
819
+ // Record system event in chat history
820
+ const displayName = formatProviderChangeText(provider, modelName)
821
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
822
+ await refreshHistory()
703
823
  } catch (reason) {
704
824
  setError(errorMessage(reason))
705
825
  }
@@ -709,7 +829,7 @@ export default function MintDashboard() {
709
829
 
710
830
  return (
711
831
  <div className={`app-container ${startupReady ? '' : 'is-loading'}`}>
712
- <div className={`app-body ${sidebarCollapsed ? 'sidebar-collapsed' : ''} ${view === 'pictures' ? 'pictures-open' : ''} ${mobileSidebarOpen ? 'mobile-sidebar-open' : ''}`}>
832
+ <div className={`app-body ${(sidebarCollapsed && window.innerWidth > 760) ? 'sidebar-collapsed' : ''} ${view === 'pictures' ? 'pictures-open' : ''} ${mobileSidebarOpen ? 'mobile-sidebar-open' : ''}`}>
713
833
  {mobileSidebarOpen && (
714
834
  <div
715
835
  className="sidebar-backdrop"
@@ -738,6 +858,8 @@ export default function MintDashboard() {
738
858
  onDeleteConversation={deleteConversation}
739
859
  onRenameConversation={renameConversation}
740
860
  onSetView={changeView}
861
+ isSearchOpen={isSearchOpen}
862
+ onSetSearchOpen={setIsSearchOpen}
741
863
  />
742
864
  <main className="assistant-workspace model-hidden">
743
865
  <ChatPanel
@@ -790,6 +912,7 @@ export default function MintDashboard() {
790
912
  setView('chat')
791
913
  setMessage(imgPrompt)
792
914
  }}
915
+ onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
793
916
  />
794
917
  </div>
795
918
  <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
@@ -804,6 +927,77 @@ export default function MintDashboard() {
804
927
  <button onClick={() => setError('')} style={{ marginLeft: '12px', background: 'transparent', border: 0, color: 'white', cursor: 'pointer' }}>✕</button>
805
928
  </div>
806
929
  )}
930
+
931
+ {isSearchOpen && (
932
+ <div className="sidebar-search-modal-backdrop" onClick={() => setIsSearchOpen(false)}>
933
+ <div className="sidebar-search-modal" onClick={(e) => e.stopPropagation()}>
934
+ <div className="search-modal-header">
935
+ <span className="search-icon-wrapper">
936
+ <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">
937
+ <circle cx="11" cy="11" r="8"></circle>
938
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
939
+ </svg>
940
+ </span>
941
+ <input
942
+ type="text"
943
+ placeholder="Search chats..."
944
+ value={searchQuery}
945
+ onChange={(e) => setSearchQuery(e.target.value)}
946
+ autoFocus
947
+ />
948
+ <button className="search-modal-close" onClick={() => setIsSearchOpen(false)}>
949
+ <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">
950
+ <line x1="18" y1="6" x2="6" y2="18"></line>
951
+ <line x1="6" y1="6" x2="18" y2="18"></line>
952
+ </svg>
953
+ </button>
954
+ </div>
955
+
956
+ <div className="search-modal-body">
957
+ <button
958
+ className="search-new-chat-btn"
959
+ onClick={() => {
960
+ clearHistory('New chat')
961
+ setIsSearchOpen(false)
962
+ }}
963
+ >
964
+ <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">
965
+ <line x1="12" y1="5" x2="12" y2="19"></line>
966
+ <line x1="5" y1="12" x2="19" y2="12"></line>
967
+ </svg>
968
+ <span>New Chat</span>
969
+ </button>
970
+
971
+ <div className="search-modal-results">
972
+ {Object.keys(groupedSearchSessions).length > 0 ? (
973
+ Object.entries(groupedSearchSessions).map(([groupName, sessions]) => (
974
+ <div key={groupName} className="search-results-group">
975
+ <div className="search-group-title">{groupName}</div>
976
+ {sessions.map((session) => (
977
+ <button
978
+ key={session.id}
979
+ className={`search-result-item ${session.id === conversationId ? 'active' : ''}`}
980
+ onClick={() => {
981
+ selectConversation(session.id)
982
+ setIsSearchOpen(false)
983
+ }}
984
+ >
985
+ <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">
986
+ <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>
987
+ </svg>
988
+ <span className="search-result-title">{session.title || 'New chat'}</span>
989
+ </button>
990
+ ))}
991
+ </div>
992
+ ))
993
+ ) : (
994
+ <div className="search-no-results">No matching chats found</div>
995
+ )}
996
+ </div>
997
+ </div>
998
+ </div>
999
+ </div>
1000
+ )}
807
1001
  </div>
808
1002
  )
809
1003
  }
@@ -39,10 +39,8 @@ export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPi
39
39
  return () => window.clearTimeout(timer)
40
40
  }, [view, pictures.length, visibleCount])
41
41
 
42
- if (view !== 'pictures') return null
43
-
44
42
  return (
45
- <section className="pictures-library">
43
+ <section className={`pictures-library ${view === 'pictures' ? 'is-visible' : ''}`} aria-hidden={view !== 'pictures'}>
46
44
  <header className="pictures-header">
47
45
  <div><span className="pictures-kicker">Gallery</span><h2>Saved Pictures</h2></div>
48
46
  <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,196 @@
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 = false
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} disabled={!isDesktopApp}>Open workflows.json</button>
167
+ <button className="btn btn-primary" onClick={handleReloadWorkflows} disabled={!isDesktopApp}>Reload Rules</button>
168
+ </div>
169
+ {!isDesktopApp && <p className="hint">Opening and reloading local workflow files requires the desktop app.</p>}
170
+ </section>
171
+
172
+ <section className="setting-section">
173
+ <div className="section-heading">
174
+ <div>
175
+ <p className="section-kicker">Code mode</p>
176
+ <h2 className="section-title">Agent Collaboration</h2>
177
+ </div>
178
+ </div>
179
+ <div className="toggle-row">
180
+ <div>
181
+ <label>Enable Multi-Agent Review</label>
182
+ <p className="hint">Allow a secondary model to review code written by the primary model.</p>
183
+ </div>
184
+ <label className="settings-toggle-switch">
185
+ <input
186
+ type="checkbox"
187
+ checked={config.enableAgentCollaboration}
188
+ onChange={(e) => updateField('enableAgentCollaboration', e.target.checked)}
189
+ />
190
+ <span className="settings-toggle-slider"></span>
191
+ </label>
192
+ </div>
193
+ </section>
194
+ </div>
195
+ )
196
+ }