@pheem49/mint 1.8.2 → 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 (106) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +297 -107
  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 +382 -127
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/image.rs +8 -8
  10. package/crates/mint-cli/src/interactive.rs +2888 -0
  11. package/crates/mint-cli/src/main.rs +391 -2506
  12. package/crates/mint-cli/src/markdown.rs +294 -0
  13. package/crates/mint-cli/src/onboard.rs +152 -109
  14. package/crates/mint-cli/src/setup.rs +143 -143
  15. package/crates/mint-cli/src/updater.rs +48 -6
  16. package/crates/mint-core/Cargo.toml +1 -0
  17. package/crates/mint-core/src/agent_loop.rs +1 -0
  18. package/crates/mint-core/src/api_server.rs +431 -65
  19. package/crates/mint-core/src/browser.rs +630 -102
  20. package/crates/mint-core/src/channels.rs +105 -6
  21. package/crates/mint-core/src/chat.rs +174 -11
  22. package/crates/mint-core/src/code_tools.rs +47 -31
  23. package/crates/mint-core/src/config.rs +302 -17
  24. package/crates/mint-core/src/image_gen.rs +299 -38
  25. package/crates/mint-core/src/lib.rs +10 -7
  26. package/crates/mint-core/src/mcp.rs +119 -6
  27. package/crates/mint-core/src/memory.rs +6 -1
  28. package/crates/mint-core/src/orchestration.rs +237 -96
  29. package/crates/mint-core/src/pictures.rs +67 -5
  30. package/crates/mint-core/src/semantic.rs +1 -1
  31. package/crates/mint-core/src/shell.rs +1 -1
  32. package/crates/mint-core/src/skills.rs +128 -25
  33. package/crates/mint-core/src/video_gen.rs +235 -0
  34. package/package.json +8 -2
  35. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  36. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  37. package/shared-modules/linux-audio/lilv.json +26 -19
  38. package/shared-modules/linux-audio/lv2.json +0 -1
  39. package/shared-modules/luajit/luajit.json +2 -2
  40. package/skills-lock.json +41 -0
  41. package/src/renderer/index-web.html +1 -1
  42. package/src/renderer/index.html +1 -1
  43. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  44. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  45. package/src/renderer/shared/platform.ts +3 -0
  46. package/src/renderer/shared/types.ts +4 -0
  47. package/src/renderer/shared/utils/approval.tsx +86 -7
  48. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  49. package/src/renderer/shared/utils/markdown.tsx +217 -88
  50. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  51. package/src/renderer/src/components/ChatPanel.tsx +486 -69
  52. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  53. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  54. package/src/renderer/src/components/MintDashboard.tsx +195 -7
  55. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  56. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  57. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  58. package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
  59. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  60. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  61. package/src/renderer/src/components/SettingsWindow.tsx +106 -3
  62. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  63. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  64. package/src/renderer/src/css/image-studio.css +106 -0
  65. package/src/renderer/src/css/settings.css +158 -0
  66. package/src/renderer/src/css/styles.css +368 -1
  67. package/src/renderer/src/css/veo-studio.css +626 -0
  68. package/src/renderer/src/index.css +1 -0
  69. package/src/renderer/src/tauri.ts +144 -17
  70. package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
  71. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  72. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  73. package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
  74. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  75. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  76. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  77. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
  78. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  79. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  80. package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
  81. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  82. package/src/renderer/src-web/css/image-studio.css +106 -0
  83. package/src/renderer/src-web/css/settings.css +158 -0
  84. package/src/renderer/src-web/css/styles.css +369 -1
  85. package/src/renderer/src-web/css/veo-studio.css +650 -0
  86. package/src/renderer/src-web/index.css +1 -0
  87. package/src/renderer/src-web/tauri.ts +135 -16
  88. package/src-tauri/src/desktop.rs +18 -16
  89. package/src-tauri/src/discord_rpc.rs +1 -1
  90. package/src-tauri/src/lib.rs +155 -25
  91. package/src-tauri/src/webhooks.rs +45 -31
  92. package/src-tauri/src/workflows.rs +1 -0
  93. package/src-tauri/tauri.conf.json +1 -1
  94. package/temp_gridgeist/AGENTS.md +8 -0
  95. package/temp_gridgeist/CHANGELOG.md +130 -0
  96. package/temp_gridgeist/LICENSE +21 -0
  97. package/temp_gridgeist/README.md +323 -0
  98. package/temp_gridgeist/README.th.md +325 -0
  99. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  100. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  101. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  102. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  103. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  104. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  105. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  106. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -1,7 +1,22 @@
1
1
  import { useEffect, useMemo, useState } from 'react'
2
- import { type PictureEntry, convertFileSrc } from '../tauri'
2
+ import { type PictureEntry, convertFileSrc, isTauriRuntime, getLocalApiBase } from '../tauri'
3
3
  import type { DashboardView } from './DashboardSidebar'
4
4
 
5
+ const getPictureSrc = (picture: PictureEntry, useThumbnail = false) => {
6
+ if (isTauriRuntime()) {
7
+ if (useThumbnail && (picture.thumbnailPath || picture.thumbnailUrl)) {
8
+ return convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || '');
9
+ }
10
+ return convertFileSrc(picture.path || picture.url || '');
11
+ } else {
12
+ const apiBase = getLocalApiBase();
13
+ if (useThumbnail && (picture.thumbnailPath || picture.thumbnailUrl)) {
14
+ return `${apiBase}/thumbnails/${picture.id}.thumb.png`;
15
+ }
16
+ return `${apiBase}/pictures/${encodeURIComponent(picture.filename)}`;
17
+ }
18
+ }
19
+
5
20
  interface PicturesLibraryProps {
6
21
  view: DashboardView
7
22
  pictures: PictureEntry[]
@@ -10,15 +25,39 @@ interface PicturesLibraryProps {
10
25
  }
11
26
 
12
27
  export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPictures }: PicturesLibraryProps) {
28
+ const [filterType, setFilterType] = useState<'photo' | 'video'>('photo')
13
29
  const [visibleCount, setVisibleCount] = useState(24)
30
+ const [failedImages, setFailedImages] = useState<Set<string>>(new Set())
31
+
32
+ const filteredPictures = useMemo(() => {
33
+ return pictures.filter((picture) => {
34
+ const pathLower = (picture.path || '').toLowerCase()
35
+ const urlLower = (picture.url || '').toLowerCase()
36
+ const mimeLower = (picture.mimeType || '').toLowerCase()
37
+
38
+ const isVideo =
39
+ mimeLower.startsWith('video/') ||
40
+ pathLower.endsWith('.mp4') ||
41
+ pathLower.endsWith('.webm') ||
42
+ pathLower.endsWith('.mov') ||
43
+ pathLower.endsWith('.avi') ||
44
+ pathLower.endsWith('.mkv') ||
45
+ urlLower.endsWith('.mp4') ||
46
+ urlLower.endsWith('.webm') ||
47
+ urlLower.endsWith('.mov')
48
+
49
+ return filterType === 'video' ? isVideo : !isVideo
50
+ })
51
+ }, [pictures, filterType])
52
+
14
53
  const visiblePictures = useMemo(
15
- () => pictures.slice(0, visibleCount),
16
- [pictures, visibleCount],
54
+ () => filteredPictures.slice(0, visibleCount),
55
+ [filteredPictures, visibleCount],
17
56
  )
18
57
 
19
58
  useEffect(() => {
20
59
  setVisibleCount(24)
21
- }, [view, pictures])
60
+ }, [view, pictures, filterType])
22
61
 
23
62
  return (
24
63
  <section className={`pictures-library ${view === 'pictures' ? 'is-visible' : ''}`} aria-hidden={view !== 'pictures'}>
@@ -31,34 +70,135 @@ export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPi
31
70
  </button>
32
71
  </div>
33
72
  </header>
34
- {pictures.length === 0 ? (
73
+
74
+ {/* Tabs Filter */}
75
+ <div className="pictures-type-filter">
76
+ <button
77
+ type="button"
78
+ className={`filter-tab-btn ${filterType === 'photo' ? 'active' : ''}`}
79
+ onClick={() => setFilterType('photo')}
80
+ >
81
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
82
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
83
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
84
+ <polyline points="21 15 16 10 5 21"></polyline>
85
+ </svg>
86
+ Photos
87
+ </button>
88
+ <button
89
+ type="button"
90
+ className={`filter-tab-btn ${filterType === 'video' ? 'active' : ''}`}
91
+ onClick={() => setFilterType('video')}
92
+ >
93
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
94
+ <polygon points="23 7 16 12 23 17 23 7"></polygon>
95
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
96
+ </svg>
97
+ Videos
98
+ </button>
99
+ </div>
100
+
101
+ {filteredPictures.length === 0 ? (
35
102
  <div className="pictures-empty">
36
103
  <div className="pictures-empty-icon" style={{ display: 'flex', justifyContent: 'center', marginBottom: '12px', opacity: 0.3 }}>
37
104
  <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
38
105
  <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline>
39
106
  </svg>
40
107
  </div>
41
- <p>No saved pictures yet</p>
42
- <span>Images appear here after a message with an attachment is sent successfully.</span>
108
+ <p>No saved {filterType === 'video' ? 'videos' : 'pictures'} yet</p>
109
+ <span>Items appear here after a message with an attachment is sent successfully.</span>
43
110
  </div>
44
111
  ) : (
45
112
  <>
46
113
  <div className="pictures-grid">
47
- {visiblePictures.map((picture, index) => (
48
- <article className="picture-card" key={picture.id}>
49
- <img src={convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || picture.path)} alt={picture.message || picture.filename} loading={index < 6 ? 'eager' : 'lazy'} decoding="async" />
50
- <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
51
- </article>
52
- ))}
114
+ {visiblePictures.map((picture, index) => {
115
+ const isVideo = filterType === 'video'
116
+ return (
117
+ <article className="picture-card" key={picture.id}>
118
+ {isVideo ? (
119
+ <div style={{ position: 'relative', width: '100%', aspectRatio: '4 / 3', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#1a1a1a' }}>
120
+ {failedImages.has(picture.id) ? (
121
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px', color: '#666' }}>
122
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.5 }}>
123
+ <path d="M23 7l-7 5 7 5V7z"></path>
124
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
125
+ </svg>
126
+ <span style={{ fontSize: '11px', fontWeight: 500, letterSpacing: '0.05em', textTransform: 'uppercase', opacity: 0.8 }}>Video</span>
127
+ </div>
128
+ ) : (
129
+ <>
130
+ {picture.thumbnailPath || picture.thumbnailUrl ? (
131
+ <img
132
+ src={getPictureSrc(picture, true)}
133
+ alt={picture.message || picture.filename}
134
+ loading={index < 6 ? 'eager' : 'lazy'}
135
+ decoding="async"
136
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
137
+ onError={() => setFailedImages((prev) => new Set([...prev, picture.id]))}
138
+ />
139
+ ) : (
140
+ <video
141
+ src={getPictureSrc(picture, false)}
142
+ preload="metadata"
143
+ style={{ width: '100%', height: '100%', objectFit: 'cover', pointerEvents: 'none' }}
144
+ onError={() => setFailedImages((prev) => new Set([...prev, picture.id]))}
145
+ />
146
+ )}
147
+ <div style={{
148
+ position: 'absolute',
149
+ top: '8px',
150
+ left: '8px',
151
+ display: 'flex',
152
+ alignItems: 'center',
153
+ gap: '4px',
154
+ backgroundColor: 'rgba(0, 0, 0, 0.75)',
155
+ color: '#fff',
156
+ padding: '4px 8px',
157
+ borderRadius: '4px',
158
+ fontSize: '11px',
159
+ fontWeight: 600,
160
+ pointerEvents: 'none',
161
+ backdropFilter: 'blur(4px)',
162
+ border: '1px solid rgba(255, 255, 255, 0.1)'
163
+ }}>
164
+ <span>📹 Video</span>
165
+ </div>
166
+ </>
167
+ )}
168
+ </div>
169
+ ) : (
170
+ <div style={{ position: 'relative', width: '100%', aspectRatio: '4 / 3', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#1a1a1a' }}>
171
+ {failedImages.has(picture.id) ? (
172
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.4 }}>
173
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
174
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
175
+ <polyline points="21 15 16 10 5 21"></polyline>
176
+ </svg>
177
+ ) : (
178
+ <img
179
+ src={getPictureSrc(picture, true)}
180
+ alt={picture.message || picture.filename}
181
+ loading={index < 6 ? 'eager' : 'lazy'}
182
+ decoding="async"
183
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
184
+ onError={() => setFailedImages((prev) => new Set([...prev, picture.id]))}
185
+ />
186
+ )}
187
+ </div>
188
+ )}
189
+ <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
190
+ </article>
191
+ )
192
+ })}
53
193
  </div>
54
- {visibleCount < pictures.length && (
194
+ {visibleCount < filteredPictures.length && (
55
195
  <div className="pictures-load-more-container">
56
196
  <button
57
197
  type="button"
58
198
  className="pictures-load-more-btn"
59
199
  onClick={() => setVisibleCount((prev) => prev + 24)}
60
200
  >
61
- Load More ({pictures.length - visibleCount} remaining)
201
+ Load More ({filteredPictures.length - visibleCount} remaining)
62
202
  </button>
63
203
  </div>
64
204
  )}
@@ -7,7 +7,8 @@ import {
7
7
  DEEPSEEK_MODELS,
8
8
  ANTHROPIC_MODELS,
9
9
  HF_MODELS,
10
- LOCAL_MODELS
10
+ LOCAL_MODELS,
11
+ CustomProviderConfig,
11
12
  } from '../SettingsWindow'
12
13
 
13
14
  interface Agent {
@@ -21,7 +22,7 @@ interface Agent {
21
22
  }
22
23
 
23
24
  interface AgentsTabProps {
24
- config: typeof DEFAULT_CONFIG & { agents?: Agent[] }
25
+ config: typeof DEFAULT_CONFIG & { agents?: Agent[], customProviders?: CustomProviderConfig[] }
25
26
  updateField: (field: any, value: any) => void
26
27
  dynamicOllamaModels?: string[]
27
28
  }
@@ -47,6 +48,11 @@ export default function AgentsTab({ config, updateField, dynamicOllamaModels = [
47
48
  else if (newProvider === 'huggingface') defaultModel = config.hfModel || HF_MODELS[0]
48
49
  else if (newProvider === 'local_openai') defaultModel = config.localModelName || LOCAL_MODELS[0]
49
50
  else if (newProvider === 'ollama') defaultModel = config.ollamaModel || dynamicOllamaModels[0] || 'llama3:latest'
51
+ else if (newProvider.startsWith('custom:')) {
52
+ const cpId = newProvider.replace(/^custom:/, '')
53
+ const cp = (config.customProviders ?? []).find(p => p.id === cpId)
54
+ defaultModel = cp?.models[0]?.modelId ?? ''
55
+ }
50
56
  setModel(defaultModel)
51
57
  }
52
58
 
@@ -141,6 +147,14 @@ export default function AgentsTab({ config, updateField, dynamicOllamaModels = [
141
147
  { value: 'ollama', label: 'Ollama (Local)', hasKey: true },
142
148
  { value: 'huggingface', label: 'Hugging Face', hasKey: !!config.hfApiKey },
143
149
  { value: 'local_openai', label: 'Local (LM Studio / OpenAI Compatible)', hasKey: true },
150
+ // Custom providers — available whenever they have a base URL configured
151
+ ...(config.customProviders ?? [])
152
+ .filter(cp => cp.baseUrl.trim().length > 0)
153
+ .map(cp => ({
154
+ value: `custom:${cp.id}`,
155
+ label: `${cp.displayName || cp.id} (Custom)`,
156
+ hasKey: true,
157
+ })),
144
158
  ].filter(p => p.hasKey);
145
159
 
146
160
  // Find out what models list corresponds to the selected provider
@@ -170,6 +184,11 @@ export default function AgentsTab({ config, updateField, dynamicOllamaModels = [
170
184
  } else if (provider === 'ollama') {
171
185
  modelList = dynamicOllamaModels
172
186
  defaultGeneralModel = config.ollamaModel
187
+ } else if (provider.startsWith('custom:')) {
188
+ const cpId = provider.replace(/^custom:/, '')
189
+ const cp = (config.customProviders ?? []).find(p => p.id === cpId)
190
+ modelList = cp?.models.map(m => m.modelId) ?? []
191
+ defaultGeneralModel = modelList[0] ?? ''
173
192
  }
174
193
 
175
194
  // Sort modelList so defaultGeneralModel is first (at the top)
@@ -119,25 +119,323 @@ export default function AutomationTab({
119
119
  <p className="section-kicker">Messaging</p>
120
120
  <h2 className="section-title">Native Channel Bridges</h2>
121
121
  </div>
122
- <p className="section-description">Configure credentials used directly by the Rust channel workers.</p>
122
+ <p className="section-description">
123
+ Configure messaging credentials and webhooks used by Rust channel workers to enable remote control.
124
+ </p>
123
125
  </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)} />
126
+
127
+ {/* Global Instant Ack Notification */}
128
+ <div className={`bridge-card ${config.enableBridgeAckNotification !== false ? 'active' : ''}`} style={{ marginBottom: '16px' }}>
129
+ <div className="bridge-card-header">
130
+ <div className="bridge-info">
131
+ <div className="bridge-icon-badge" style={{ background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)' }}>
132
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
133
+ <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
134
+ </svg>
135
+ </div>
136
+ <div className="bridge-title-group">
137
+ <span className="bridge-title">
138
+ Instant Remote Acknowledgement
139
+ <span className={`bridge-status-pill ${config.enableBridgeAckNotification !== false ? 'active' : 'inactive'}`}>
140
+ {config.enableBridgeAckNotification !== false ? 'Enabled' : 'Disabled'}
141
+ </span>
142
+ </span>
143
+ <span className="bridge-subtitle">Send instant feedback/typing status when remote commands arrive</span>
144
+ </div>
145
+ </div>
146
+ <label className="settings-toggle-switch">
147
+ <input
148
+ type="checkbox"
149
+ checked={config.enableBridgeAckNotification !== false}
150
+ onChange={(e) => updateField('enableBridgeAckNotification', e.target.checked)}
151
+ />
152
+ <span className="settings-toggle-slider"></span>
153
+ </label>
154
+ </div>
155
+ {config.enableBridgeAckNotification !== false && (
156
+ <div className="bridge-inputs">
157
+ <input
158
+ type="text"
159
+ className="bridge-input"
160
+ placeholder="Instant Ack Message (default: [Mint Agent] Remote command received, processing...)"
161
+ value={config.bridgeAckMessage || ''}
162
+ onChange={(e) => updateField('bridgeAckMessage', e.target.value)}
163
+ />
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ <div className="bridge-cards-container">
169
+ {/* Telegram Bot Card */}
170
+ <div className={`bridge-card ${config.enableTelegramBridge ? 'active' : ''}`}>
171
+ <div className="bridge-card-header">
172
+ <div className="bridge-info">
173
+ <div className="bridge-icon-badge bridge-icon-telegram">
174
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
175
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69.01-.03.01-.14-.07-.2-.08-.06-.19-.04-.27-.02-.12.02-1.96 1.25-5.54 3.69-.52.36-1 .54-1.42.53-.47-.01-1.37-.26-2.03-.48-.82-.27-1.47-.42-1.42-.88.03-.25.38-.51 1.07-.78 4.18-1.82 6.97-3.02 8.37-3.61 3.98-1.66 4.81-1.95 5.35-1.96.12 0 .38.03.55.17.14.12.18.28.2.4.02.11.02.26.01.39z"/>
176
+ </svg>
177
+ </div>
178
+ <div className="bridge-title-group">
179
+ <span className="bridge-title">
180
+ Telegram Bot API
181
+ <span className={`bridge-status-pill ${config.enableTelegramBridge ? 'active' : 'inactive'}`}>
182
+ {config.enableTelegramBridge ? 'Active' : 'Disabled'}
183
+ </span>
184
+ </span>
185
+ <span className="bridge-subtitle">Long Polling Engine (getUpdates)</span>
186
+ </div>
187
+ </div>
188
+ <label className="settings-toggle-switch">
189
+ <input
190
+ type="checkbox"
191
+ checked={config.enableTelegramBridge}
192
+ onChange={(e) => updateField('enableTelegramBridge', e.target.checked)}
193
+ />
194
+ <span className="settings-toggle-slider"></span>
195
+ </label>
196
+ </div>
197
+ <div className="bridge-inputs">
198
+ <input
199
+ type="password"
200
+ className="bridge-input"
201
+ placeholder="Telegram Bot Token (from @BotFather)"
202
+ value={config.telegramBotToken}
203
+ onChange={(e) => updateField('telegramBotToken', e.target.value)}
204
+ />
205
+ </div>
206
+ </div>
207
+
208
+ {/* Discord Gateway Card */}
209
+ <div className={`bridge-card ${config.enableDiscordBridge ? 'active' : ''}`}>
210
+ <div className="bridge-card-header">
211
+ <div className="bridge-info">
212
+ <div className="bridge-icon-badge bridge-icon-discord">
213
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
214
+ <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.061 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.893.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
215
+ </svg>
216
+ </div>
217
+ <div className="bridge-title-group">
218
+ <span className="bridge-title">
219
+ Discord Gateway
220
+ <span className={`bridge-status-pill ${config.enableDiscordBridge ? 'active' : 'inactive'}`}>
221
+ {config.enableDiscordBridge ? 'Active' : 'Disabled'}
222
+ </span>
223
+ </span>
224
+ <span className="bridge-subtitle">WebSocket Gateway (wss://gateway.discord.gg)</span>
225
+ </div>
226
+ </div>
227
+ <label className="settings-toggle-switch">
228
+ <input
229
+ type="checkbox"
230
+ checked={config.enableDiscordBridge}
231
+ onChange={(e) => updateField('enableDiscordBridge', e.target.checked)}
232
+ />
233
+ <span className="settings-toggle-slider"></span>
234
+ </label>
235
+ </div>
236
+ <div className="bridge-inputs">
237
+ <input
238
+ type="password"
239
+ className="bridge-input"
240
+ placeholder="Discord Bot Token"
241
+ value={config.discordBotToken}
242
+ onChange={(e) => updateField('discordBotToken', e.target.value)}
243
+ />
244
+ <input
245
+ type="text"
246
+ className="bridge-input"
247
+ placeholder="Discord Application ID (for Rich Presence)"
248
+ value={config.discordApplicationId}
249
+ onChange={(e) => updateField('discordApplicationId', e.target.value)}
250
+ />
251
+ </div>
252
+ </div>
253
+
254
+ {/* Slack Socket Mode Card */}
255
+ <div className={`bridge-card ${config.enableSlackBridge ? 'active' : ''}`}>
256
+ <div className="bridge-card-header">
257
+ <div className="bridge-info">
258
+ <div className="bridge-icon-badge bridge-icon-slack">
259
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
260
+ <path d="M6 15a2.5 2.5 0 0 1-2.5-2.5A2.5 2.5 0 0 1 6 10h2.5v2.5A2.5 2.5 0 0 1 6 15zm0-8.5A2.5 2.5 0 0 1 3.5 4 2.5 2.5 0 0 1 6 1.5 2.5 2.5 0 0 1 8.5 4v2.5H6zm8.5 0A2.5 2.5 0 0 1 12 4a2.5 2.5 0 0 1 2.5-2.5A2.5 2.5 0 0 1 17 4v2.5h-2.5zm0 8.5a2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5A2.5 2.5 0 0 1 12 17.5V15h2.5zm-8.5 2.5A2.5 2.5 0 0 1 6 20a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5H8.5V17.5zm8.5-8.5A2.5 2.5 0 0 1 17 6.5 2.5 2.5 0 0 1 19.5 9 2.5 2.5 0 0 1 17 11.5H14.5V9zm-2.5-2.5a2.5 2.5 0 0 1-2.5-2.5A2.5 2.5 0 0 1 12 1.5a2.5 2.5 0 0 1 2.5 2.5V6.5H12z"/>
261
+ </svg>
262
+ </div>
263
+ <div className="bridge-title-group">
264
+ <span className="bridge-title">
265
+ Slack Socket Mode
266
+ <span className={`bridge-status-pill ${config.enableSlackBridge ? 'active' : 'inactive'}`}>
267
+ {config.enableSlackBridge ? 'Active' : 'Disabled'}
268
+ </span>
269
+ </span>
270
+ <span className="bridge-subtitle">WebSocket Socket Mode API</span>
271
+ </div>
272
+ </div>
273
+ <label className="settings-toggle-switch">
274
+ <input
275
+ type="checkbox"
276
+ checked={config.enableSlackBridge}
277
+ onChange={(e) => updateField('enableSlackBridge', e.target.checked)}
278
+ />
279
+ <span className="settings-toggle-slider"></span>
280
+ </label>
281
+ </div>
282
+ <div className="bridge-inputs">
283
+ <input
284
+ type="password"
285
+ className="bridge-input"
286
+ placeholder="Slack Bot Token (xoxb-...)"
287
+ value={config.slackBotToken}
288
+ onChange={(e) => updateField('slackBotToken', e.target.value)}
289
+ />
290
+ <input
291
+ type="password"
292
+ className="bridge-input"
293
+ placeholder="Slack App Token (xapp-...)"
294
+ value={config.slackAppToken}
295
+ onChange={(e) => updateField('slackAppToken', e.target.value)}
296
+ />
297
+ </div>
298
+ </div>
299
+
300
+ {/* LINE Webhook Card */}
301
+ <div className={`bridge-card ${config.enableLineBridge ? 'active' : ''}`}>
302
+ <div className="bridge-card-header">
303
+ <div className="bridge-info">
304
+ <div className="bridge-icon-badge bridge-icon-line">
305
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
306
+ <path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63h-2.286v1.442h2.286c.349 0 .63.285.63.63 0 .346-.281.63-.63.63h-2.917c-.349 0-.63-.284-.63-.63V8.581c0-.346.281-.63.63-.63h2.917c.349 0 .63.284.63.63 0 .346-.281.63-.63.63h-2.286V9.86h2.286zm-5.631 4.156c0 .248-.146.472-.371.57-.083.036-.172.054-.26.054-.154 0-.306-.056-.425-.165l-2.481-3.238v2.783c0 .346-.28.63-.63.63-.348 0-.63-.284-.63-.63V8.581c0-.248.146-.472.371-.57.225-.098.486-.051.664.12l2.48 3.237V8.581c0-.346.28-.63.63-.63.349 0 .63.284.63.63v5.438zm-6.843 0c0 .346-.28.63-.63.63-.349 0-.63-.284-.63-.63V8.581c0-.346.281-.63.63-.63.35 0 .63.284.63.63v5.438zm-2.434 0c0 .346-.28.63-.63.63h-2.417c-.349 0-.63-.284-.63-.63V8.581c0-.346.281-.63.63-.63.349 0 .63.284.63.63v4.808h1.787c.349 0 .63.285.63.63zM24 10.617C24 4.754 18.627 0 12 0S0 4.754 0 10.617c0 5.247 4.264 9.61 10.02 10.435.39.084.922.257 1.057.592.121.3.079.77.039 1.074l-.17.994c-.052.304-.247 1.191 1.044.649 1.292-.542 6.974-4.107 9.513-7.032C23.013 15.602 24 13.23 24 10.617z"/>
307
+ </svg>
308
+ </div>
309
+ <div className="bridge-title-group">
310
+ <span className="bridge-title">
311
+ LINE Messaging Webhook
312
+ <span className={`bridge-status-pill ${config.enableLineBridge ? 'active' : 'inactive'}`}>
313
+ {config.enableLineBridge ? 'Active' : 'Disabled'}
314
+ </span>
315
+ </span>
316
+ <span className="bridge-subtitle">HTTP Webhook Listener (Callback)</span>
317
+ </div>
318
+ </div>
319
+ <label className="settings-toggle-switch">
320
+ <input
321
+ type="checkbox"
322
+ checked={config.enableLineBridge}
323
+ onChange={(e) => updateField('enableLineBridge', e.target.checked)}
324
+ />
325
+ <span className="settings-toggle-slider"></span>
326
+ </label>
327
+ </div>
328
+ <div className="bridge-inputs">
329
+ <input
330
+ type="password"
331
+ className="bridge-input"
332
+ placeholder="LINE Channel Access Token"
333
+ value={config.lineChannelAccessToken}
334
+ onChange={(e) => updateField('lineChannelAccessToken', e.target.value)}
335
+ />
336
+ <input
337
+ type="password"
338
+ className="bridge-input"
339
+ placeholder="LINE Channel Secret"
340
+ value={config.lineChannelSecret}
341
+ onChange={(e) => updateField('lineChannelSecret', e.target.value)}
342
+ />
343
+ <div className="bridge-input-row">
344
+ <input
345
+ type="text"
346
+ className="bridge-input"
347
+ placeholder="Webhook Host (default: 127.0.0.1)"
348
+ value={config.lineWebhookHost || ''}
349
+ onChange={(e) => updateField('lineWebhookHost', e.target.value)}
350
+ />
351
+ <input
352
+ type="number"
353
+ className="bridge-input"
354
+ placeholder="Port"
355
+ value={config.lineWebhookPort || 3000}
356
+ onChange={(e) => updateField('lineWebhookPort', parseInt(e.target.value) || 3000)}
357
+ style={{ width: '120px' }}
358
+ />
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ {/* WhatsApp Cloud API Card */}
364
+ <div className={`bridge-card ${config.enableWhatsappBridge ? 'active' : ''}`}>
365
+ <div className="bridge-card-header">
366
+ <div className="bridge-info">
367
+ <div className="bridge-icon-badge bridge-icon-whatsapp">
368
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
369
+ <path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.893 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.301-.15-1.785-.881-2.062-.982-.276-.101-.477-.15-.677.15-.199.301-.776.982-.952 1.182-.175.201-.351.226-.652.076-.301-.15-1.27-.468-2.42-1.494-.897-.8-1.502-1.788-1.678-2.09-.175-.301-.019-.464.131-.613.136-.135.301-.351.451-.526.15-.176.2-.301.3-.502.1-.201.05-.376-.025-.526-.075-.15-.677-1.631-.928-2.235-.244-.587-.492-.507-.676-.517-.175-.008-.376-.01-.576-.01-.201 0-.526.075-.802.376-.276.301-1.053 1.029-1.053 2.509 0 1.48 1.078 2.909 1.229 3.11.15.201 2.122 3.241 5.141 4.544.718.31 1.279.495 1.716.634.721.229 1.377.197 1.896.12.578-.086 1.785-.729 2.036-1.431.25-.702.25-1.304.175-1.431-.075-.127-.276-.201-.577-.352z"/>
370
+ </svg>
371
+ </div>
372
+ <div className="bridge-title-group">
373
+ <span className="bridge-title">
374
+ WhatsApp Cloud API
375
+ <span className={`bridge-status-pill ${config.enableWhatsappBridge ? 'active' : 'inactive'}`}>
376
+ {config.enableWhatsappBridge ? 'Active' : 'Disabled'}
377
+ </span>
378
+ </span>
379
+ <span className="bridge-subtitle">Meta Cloud API Webhook Listener</span>
380
+ </div>
381
+ </div>
382
+ <label className="settings-toggle-switch">
383
+ <input
384
+ type="checkbox"
385
+ checked={config.enableWhatsappBridge}
386
+ onChange={(e) => updateField('enableWhatsappBridge', e.target.checked)}
387
+ />
388
+ <span className="settings-toggle-slider"></span>
389
+ </label>
390
+ </div>
391
+ <div className="bridge-inputs">
392
+ <input
393
+ type="password"
394
+ className="bridge-input"
395
+ placeholder="WhatsApp Cloud Access Token"
396
+ value={config.whatsappCloudAccessToken}
397
+ onChange={(e) => updateField('whatsappCloudAccessToken', e.target.value)}
398
+ />
399
+ <input
400
+ type="text"
401
+ className="bridge-input"
402
+ placeholder="WhatsApp Phone Number ID"
403
+ value={config.whatsappPhoneNumberId}
404
+ onChange={(e) => updateField('whatsappPhoneNumberId', e.target.value)}
405
+ />
406
+ <input
407
+ type="password"
408
+ className="bridge-input"
409
+ placeholder="WhatsApp Verify Token"
410
+ value={config.whatsappVerifyToken}
411
+ onChange={(e) => updateField('whatsappVerifyToken', e.target.value)}
412
+ />
413
+ <input
414
+ type="password"
415
+ className="bridge-input"
416
+ placeholder="WhatsApp App Secret (optional HMAC validation)"
417
+ value={config.whatsappAppSecret}
418
+ onChange={(e) => updateField('whatsappAppSecret', e.target.value)}
419
+ />
420
+ <div className="bridge-input-row">
421
+ <input
422
+ type="text"
423
+ className="bridge-input"
424
+ placeholder="Webhook Host (default: 127.0.0.1)"
425
+ value={config.whatsappWebhookHost || ''}
426
+ onChange={(e) => updateField('whatsappWebhookHost', e.target.value)}
427
+ />
428
+ <input
429
+ type="number"
430
+ className="bridge-input"
431
+ placeholder="Port"
432
+ value={config.whatsappWebhookPort || 3001}
433
+ onChange={(e) => updateField('whatsappWebhookPort', parseInt(e.target.value) || 3001)}
434
+ style={{ width: '120px' }}
435
+ />
436
+ </div>
437
+ </div>
438
+ </div>
141
439
  </div>
142
440
  </section>
143
441