@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,
@@ -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",
@@ -266,6 +268,80 @@ export default function MintDashboard() {
266
268
  const [chatSessions, setChatSessions] = useState<ChatSession[]>([])
267
269
  const chatEnd = useRef<HTMLDivElement | null>(null)
268
270
  const startupReady = (dashboardDataReady && modelReady) || startupTimedOut
271
+ const [proactiveSuggestion, setProactiveSuggestion] = useState<any>(null)
272
+
273
+ const [isSearchOpen, setIsSearchOpen] = useState(false)
274
+ const [searchQuery, setSearchQuery] = useState('')
275
+
276
+ useEffect(() => {
277
+ function handleKeyDown(event: KeyboardEvent) {
278
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
279
+ event.preventDefault()
280
+ setIsSearchOpen((prev) => !prev)
281
+ } else if (event.key === 'Escape') {
282
+ setIsSearchOpen(false)
283
+ }
284
+ }
285
+ window.addEventListener('keydown', handleKeyDown)
286
+ return () => window.removeEventListener('keydown', handleKeyDown)
287
+ }, [])
288
+
289
+ useEffect(() => {
290
+ if (!isSearchOpen) {
291
+ setSearchQuery('')
292
+ }
293
+ }, [isSearchOpen])
294
+
295
+ const filteredSessions = chatSessions.filter((session) => {
296
+ if (session.kind === 'cli' || session.id === 'conversation-default') return false
297
+ return session.title.toLowerCase().includes(searchQuery.toLowerCase())
298
+ })
299
+
300
+ const groupSessionsByDate = (sessions: ChatSession[]) => {
301
+ const groups: { [key: string]: ChatSession[] } = {}
302
+
303
+ sessions.forEach((session) => {
304
+ const dateStr = session.updatedAt || session.createdAt
305
+ if (!dateStr) {
306
+ const groupName = 'Other'
307
+ if (!groups[groupName]) groups[groupName] = []
308
+ groups[groupName].push(session)
309
+ return
310
+ }
311
+
312
+ const date = new Date(dateStr)
313
+ const today = new Date()
314
+ const yesterday = new Date()
315
+ yesterday.setDate(today.getDate() - 1)
316
+
317
+ let groupName = ''
318
+ if (date.toDateString() === today.toDateString()) {
319
+ groupName = 'Today'
320
+ } else if (date.toDateString() === yesterday.toDateString()) {
321
+ groupName = 'Yesterday'
322
+ } else {
323
+ const diffTime = Math.abs(today.getTime() - date.getTime())
324
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
325
+ if (diffDays <= 7) {
326
+ groupName = 'Previous 7 days'
327
+ } else if (diffDays <= 30) {
328
+ groupName = 'Previous 30 days'
329
+ } else {
330
+ groupName = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
331
+ }
332
+ }
333
+
334
+ if (!groups[groupName]) {
335
+ groups[groupName] = []
336
+ }
337
+ groups[groupName].push(session)
338
+ })
339
+
340
+ return groups
341
+ }
342
+
343
+ const groupedSearchSessions = groupSessionsByDate(filteredSessions)
344
+
269
345
 
270
346
  async function refreshHistory() {
271
347
  const history = await getRecentInteractions(50, conversationId)
@@ -344,10 +420,14 @@ export default function MintDashboard() {
344
420
  setPendingApproval(event.payload)
345
421
  }
346
422
  })
423
+ const unlistenProactive = window.api.onProactiveSuggestion?.((suggestion: any) => {
424
+ setProactiveSuggestion(suggestion)
425
+ })
347
426
  return () => {
348
427
  unlistenPromise?.then?.((unlisten) => unlisten?.())
349
428
  unlistenSpotlight?.then?.((unlisten) => unlisten?.())
350
429
  unlistenVision?.then?.((unlisten) => unlisten?.())
430
+ unlistenProactive?.then?.((unlisten) => unlisten?.())
351
431
  }
352
432
  }, [])
353
433
 
@@ -361,7 +441,7 @@ export default function MintDashboard() {
361
441
  }, [view])
362
442
 
363
443
  useEffect(() => {
364
- if (view === 'workspeac' && !agentMode) updateAgentMode(true)
444
+ if (view === 'workspace' && !agentMode) updateAgentMode(true)
365
445
  }, [view, agentMode])
366
446
 
367
447
  useEffect(() => {
@@ -427,14 +507,14 @@ export default function MintDashboard() {
427
507
  }
428
508
  }, [sending])
429
509
 
430
- async function handleApproval(approved: boolean, autoApproveSession = false) {
510
+ async function handleApproval(approved: boolean, autoApproveSession = false, answer?: string) {
431
511
  if (!pendingApproval) return
432
512
  try {
433
513
  if (autoApproveSession) {
434
514
  sessionAutoApprovedRef.current = true
435
515
  setSessionAutoApproved(true)
436
516
  }
437
- await submitToolApproval(pendingApproval.token, approved)
517
+ await submitToolApproval(pendingApproval.token, approved, answer)
438
518
  } catch (reason) {
439
519
  setError(errorMessage(reason))
440
520
  } finally {
@@ -533,6 +613,17 @@ export default function MintDashboard() {
533
613
  }
534
614
  }
535
615
 
616
+ const handleProactiveAction = async (action: any) => {
617
+ setProactiveSuggestion(null)
618
+ if (action && action.type !== 'none') {
619
+ try {
620
+ await window.api.executeProactiveAction(action)
621
+ } catch (err) {
622
+ setError(String(err))
623
+ }
624
+ }
625
+ }
626
+
536
627
  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
537
628
  event.preventDefault()
538
629
  const trimmed = message.trim()
@@ -674,7 +765,7 @@ export default function MintDashboard() {
674
765
  const selected = await selectWorkspaceDirectory()
675
766
  if (selected) {
676
767
  updateWorkspacePath(selected)
677
- setView('workspeac')
768
+ setView('workspace')
678
769
  }
679
770
  } catch (reason) {
680
771
  setError(errorMessage(reason))
@@ -781,6 +872,37 @@ export default function MintDashboard() {
781
872
  }
782
873
  }
783
874
 
875
+ function formatProviderChangeText(provider: string, model: string) {
876
+ let providerName = provider
877
+ if (provider === 'gemini') providerName = 'Gemini'
878
+ else if (provider === 'openai') providerName = 'OpenAI'
879
+ else if (provider === 'openrouter') providerName = 'OpenRouter'
880
+ else if (provider === 'deepseek') providerName = 'DeepSeek'
881
+ else if (provider === 'anthropic') providerName = 'Claude'
882
+ else if (provider === 'huggingface') providerName = 'HF'
883
+ else if (provider === 'local_openai') providerName = 'Local'
884
+ else if (provider === 'ollama') providerName = 'Ollama'
885
+
886
+ if (providerName && providerName === provider) {
887
+ providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
888
+ }
889
+ return [providerName, model].filter(Boolean).join(' • ')
890
+ }
891
+
892
+ function getActiveModelName(config: any, provider: string) {
893
+ switch (provider) {
894
+ case 'gemini': return config.geminiModel || 'gemini-1.5-flash'
895
+ case 'openai': return config.openaiModel || 'gpt-4o'
896
+ case 'openrouter': return config.openrouterModel || 'anthropic/claude-3.5-sonnet'
897
+ case 'deepseek': return config.deepseekModel || 'deepseek-chat'
898
+ case 'anthropic': return config.anthropicModel || 'claude-3-5-sonnet-20240620'
899
+ case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
900
+ case 'local_openai': return config.localModelName || 'llama3'
901
+ case 'ollama': return config.ollamaModel || 'llama3:latest'
902
+ default: return ''
903
+ }
904
+ }
905
+
784
906
  async function changeProvider(provider: string) {
785
907
  try {
786
908
  const config = await window.settingsApi.getSettings()
@@ -788,6 +910,12 @@ export default function MintDashboard() {
788
910
  await window.settingsApi.saveSettings(config)
789
911
  setSettingsConfig(config)
790
912
  setStatus(await getRuntimeStatus())
913
+
914
+ // Record system event in chat history
915
+ const activeModel = getActiveModelName(config, provider)
916
+ const displayName = formatProviderChangeText(provider, activeModel)
917
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
918
+ await refreshHistory()
791
919
  } catch (reason) {
792
920
  setError(errorMessage(reason))
793
921
  }
@@ -817,6 +945,11 @@ export default function MintDashboard() {
817
945
  await window.settingsApi.saveSettings(config)
818
946
  setSettingsConfig(config)
819
947
  setStatus(await getRuntimeStatus())
948
+
949
+ // Record system event in chat history
950
+ const displayName = formatProviderChangeText(provider, modelName)
951
+ await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
952
+ await refreshHistory()
820
953
  } catch (reason) {
821
954
  setError(errorMessage(reason))
822
955
  }
@@ -897,9 +1030,33 @@ export default function MintDashboard() {
897
1030
  onSetInteractionEnabled={updateInteractionEnabled}
898
1031
  onSetShowInteractionGuide={updateInteractionGuide}
899
1032
  onShowToast={showToast}
1033
+ isSearchOpen={isSearchOpen}
1034
+ onSetSearchOpen={setIsSearchOpen}
900
1035
  />
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' && (
1036
+ <main className={`assistant-workspace ${layoutPreset === 'chat-wide' ? 'layout-chat-wide' : 'layout-model-wide'} ${modelVisible || view === 'workspace' ? '' : 'model-hidden'} ${view === 'workspace' ? 'workspace-open' : ''}`}>
1037
+ {proactiveSuggestion && (
1038
+ <div className="proactive-bar" style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100 }}>
1039
+ <div className="proactive-header">
1040
+ <span className="proactive-icon">✨</span>
1041
+ <div className="proactive-message">{proactiveSuggestion.message}</div>
1042
+ <button className="proactive-dismiss-btn" onClick={() => setProactiveSuggestion(null)}>
1043
+ Dismiss
1044
+ </button>
1045
+ </div>
1046
+ <div className="proactive-chips">
1047
+ {proactiveSuggestion.suggestions?.map((sug: any, i: number) => (
1048
+ <button
1049
+ key={i}
1050
+ className="suggestion-chip"
1051
+ onClick={() => handleProactiveAction(sug.action)}
1052
+ >
1053
+ {sug.label}
1054
+ </button>
1055
+ ))}
1056
+ </div>
1057
+ </div>
1058
+ )}
1059
+ {view === 'workspace' && (
903
1060
  <WorkspacePanel
904
1061
  agentMode={agentMode}
905
1062
  sending={sending}
@@ -914,7 +1071,7 @@ export default function MintDashboard() {
914
1071
  expressionIndex={expressionIndex}
915
1072
  accessoryIndex={accessoryIndex}
916
1073
  isLocked={isLocked}
917
- isActive={modelVisible && view !== 'pictures' && view !== 'workspeac'}
1074
+ isActive={modelVisible && view !== 'pictures' && view !== 'workspace' && view !== 'workflows'}
918
1075
  layoutPreset={layoutPreset}
919
1076
  sending={sending}
920
1077
  interactionEnabled={interactionEnabled}
@@ -980,6 +1137,10 @@ export default function MintDashboard() {
980
1137
  setMessage(imgPrompt)
981
1138
  }}
982
1139
  />
1140
+ <WorkflowBuilderPanel
1141
+ view={view}
1142
+ onShowToast={showToast}
1143
+ />
983
1144
  </div>
984
1145
  <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
985
1146
  <div className="startup-loading-content">
@@ -998,6 +1159,77 @@ export default function MintDashboard() {
998
1159
  </button>
999
1160
  </div>
1000
1161
  )}
1162
+
1163
+ {isSearchOpen && (
1164
+ <div className="sidebar-search-modal-backdrop" onClick={() => setIsSearchOpen(false)}>
1165
+ <div className="sidebar-search-modal" onClick={(e) => e.stopPropagation()}>
1166
+ <div className="search-modal-header">
1167
+ <span className="search-icon-wrapper">
1168
+ <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">
1169
+ <circle cx="11" cy="11" r="8"></circle>
1170
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
1171
+ </svg>
1172
+ </span>
1173
+ <input
1174
+ type="text"
1175
+ placeholder="Search chats..."
1176
+ value={searchQuery}
1177
+ onChange={(e) => setSearchQuery(e.target.value)}
1178
+ autoFocus
1179
+ />
1180
+ <button className="search-modal-close" onClick={() => setIsSearchOpen(false)}>
1181
+ <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">
1182
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1183
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1184
+ </svg>
1185
+ </button>
1186
+ </div>
1187
+
1188
+ <div className="search-modal-body">
1189
+ <button
1190
+ className="search-new-chat-btn"
1191
+ onClick={() => {
1192
+ clearHistory('New chat')
1193
+ setIsSearchOpen(false)
1194
+ }}
1195
+ >
1196
+ <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">
1197
+ <line x1="12" y1="5" x2="12" y2="19"></line>
1198
+ <line x1="5" y1="12" x2="19" y2="12"></line>
1199
+ </svg>
1200
+ <span>New Chat</span>
1201
+ </button>
1202
+
1203
+ <div className="search-modal-results">
1204
+ {Object.keys(groupedSearchSessions).length > 0 ? (
1205
+ Object.entries(groupedSearchSessions).map(([groupName, sessions]) => (
1206
+ <div key={groupName} className="search-results-group">
1207
+ <div className="search-group-title">{groupName}</div>
1208
+ {sessions.map((session) => (
1209
+ <button
1210
+ key={session.id}
1211
+ className={`search-result-item ${session.id === conversationId ? 'active' : ''}`}
1212
+ onClick={() => {
1213
+ selectConversation(session.id)
1214
+ setIsSearchOpen(false)
1215
+ }}
1216
+ >
1217
+ <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">
1218
+ <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>
1219
+ </svg>
1220
+ <span className="search-result-title">{session.title || 'New chat'}</span>
1221
+ </button>
1222
+ ))}
1223
+ </div>
1224
+ ))
1225
+ ) : (
1226
+ <div className="search-no-results">No matching chats found</div>
1227
+ )}
1228
+ </div>
1229
+ </div>
1230
+ </div>
1231
+ </div>
1232
+ )}
1001
1233
  </div>
1002
1234
  )
1003
1235
  }
@@ -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
+ }