@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,6 +1,6 @@
1
1
  import { useState } from 'react'
2
2
 
3
- export type DashboardView = 'chat' | 'pictures' | 'model' | 'imagine' | 'workflows'
3
+ export type DashboardView = 'chat' | 'pictures' | 'model' | 'imagine' | 'workflows' | 'veo'
4
4
 
5
5
  interface ChatSessionItem {
6
6
  id: string
@@ -119,6 +119,21 @@ export default function DashboardSidebar({
119
119
  </span>
120
120
  <span>Image Studio</span>
121
121
  </button>
122
+ <button className={`sidebar-top-action ${view === 'veo' ? 'is-active' : ''}`} onClick={() => onSetView('veo')}>
123
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
124
+ <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">
125
+ <rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect>
126
+ <line x1="7" y1="2" x2="7" y2="22"></line>
127
+ <line x1="17" y1="2" x2="17" y2="22"></line>
128
+ <line x1="2" y1="12" x2="22" y2="12"></line>
129
+ <line x1="2" y1="7" x2="7" y2="7"></line>
130
+ <line x1="2" y1="17" x2="7" y2="17"></line>
131
+ <line x1="17" y1="17" x2="22" y2="17"></line>
132
+ <line x1="17" y1="7" x2="22" y2="7"></line>
133
+ </svg>
134
+ </span>
135
+ <span>Veo Studio</span>
136
+ </button>
122
137
 
123
138
  <div className="sidebar-section">
124
139
  <div className="sidebar-section-title">Conversation CLI</div>
@@ -55,9 +55,10 @@ const PROVIDER_MODELS: Record<string, { value: string; label: string }[]> = {
55
55
  ],
56
56
  replicate: [
57
57
  { value: 'black-forest-labs/flux-1.1-pro', label: 'FLUX 1.1 Pro (Default)' },
58
+ { value: 'timbrooks/instruct-pix2pix', label: 'InstructPix2Pix (Image Editing)' },
59
+ { value: 'black-forest-labs/flux-fill-dev', label: 'FLUX Fill (Inpainting)' },
58
60
  { value: 'black-forest-labs/flux-schnell', label: 'FLUX Schnell (fast)' },
59
61
  { value: 'stability-ai/sdxl', label: 'SDXL' },
60
- { value: 'bytedance/sdxl-lightning-4step', label: 'SDXL Lightning' },
61
62
  ],
62
63
  }
63
64
 
@@ -146,6 +147,36 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
146
147
 
147
148
  const effectiveModel = selectedModel === 'custom' ? customModel.trim() : selectedModel
148
149
 
150
+ const [inputImage, setInputImage] = useState<string | null>(null)
151
+ const [isDragging, setIsDragging] = useState(false)
152
+ const fileInputRef = useRef<HTMLInputElement>(null)
153
+
154
+ const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
155
+ const file = e.target.files?.[0]
156
+ if (!file) return
157
+ const reader = new FileReader()
158
+ reader.onload = (evt) => {
159
+ if (typeof evt.target?.result === 'string') {
160
+ setInputImage(evt.target.result)
161
+ }
162
+ }
163
+ reader.readAsDataURL(file)
164
+ }
165
+
166
+ const handleDrop = (e: React.DragEvent) => {
167
+ e.preventDefault()
168
+ setIsDragging(false)
169
+ const file = e.dataTransfer.files?.[0]
170
+ if (!file || !file.type.startsWith('image/')) return
171
+ const reader = new FileReader()
172
+ reader.onload = (evt) => {
173
+ if (typeof evt.target?.result === 'string') {
174
+ setInputImage(evt.target.result)
175
+ }
176
+ }
177
+ reader.readAsDataURL(file)
178
+ }
179
+
149
180
  const handleGenerate = useCallback(async () => {
150
181
  const trimmed = prompt.trim()
151
182
  if (!trimmed || generating) return
@@ -161,6 +192,8 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
161
192
  numImages,
162
193
  model: effectiveModel || undefined,
163
194
  provider: selectedProvider,
195
+ imageDataUri: inputImage || undefined,
196
+ mode: inputImage ? 'edit' : 'generate',
164
197
  }
165
198
 
166
199
  try {
@@ -176,7 +209,7 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
176
209
  } finally {
177
210
  setGenerating(false)
178
211
  }
179
- }, [prompt, negativePrompt, aspectRatio, numImages, generating, onRefreshPictures, selectedProvider, effectiveModel])
212
+ }, [prompt, negativePrompt, aspectRatio, numImages, generating, onRefreshPictures, selectedProvider, effectiveModel, inputImage])
180
213
 
181
214
  const handleKeyDown = (e: React.KeyboardEvent) => {
182
215
  if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
@@ -299,11 +332,61 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
299
332
  </div>
300
333
  )}
301
334
 
335
+ {/* Image Editing / Source Image Attachment */}
336
+ <div className="img-studio-field">
337
+ <label className="img-studio-label">
338
+ Source Image (Optional for Image Editing)
339
+ <span className="img-studio-label-hint">Upload an image to edit / inpaint</span>
340
+ </label>
341
+ {inputImage ? (
342
+ <div className="img-studio-preview-card">
343
+ <img src={inputImage} alt="Input source" className="img-studio-preview-thumb" />
344
+ <div className="img-studio-preview-info">
345
+ <div className="img-studio-preview-title">
346
+ <span>Source Image Attached</span>
347
+ </div>
348
+ <div className="img-studio-preview-subtitle">Ready for Image Editing</div>
349
+ </div>
350
+ <button
351
+ type="button"
352
+ className="img-studio-preview-remove"
353
+ onClick={() => setInputImage(null)}
354
+ >
355
+ ✕ Remove
356
+ </button>
357
+ </div>
358
+ ) : (
359
+ <div
360
+ className={`img-studio-dropzone ${isDragging ? 'is-dragging' : ''}`}
361
+ onClick={() => fileInputRef.current?.click()}
362
+ onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
363
+ onDragLeave={() => setIsDragging(false)}
364
+ onDrop={handleDrop}
365
+ >
366
+ <input
367
+ ref={fileInputRef}
368
+ type="file"
369
+ accept="image/*"
370
+ onChange={handleImageUpload}
371
+ disabled={generating}
372
+ style={{ display: 'none' }}
373
+ />
374
+ <svg className="img-studio-dropzone-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
375
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
376
+ <polyline points="17 8 12 3 7 8"/>
377
+ <line x1="12" y1="3" x2="12" y2="15"/>
378
+ </svg>
379
+ <div className="img-studio-dropzone-text">Click or Drag & Drop image here</div>
380
+ <div className="img-studio-dropzone-subtext">PNG, JPG, WEBP up to 10MB</div>
381
+ </div>
382
+ )}
383
+ </div>
384
+
302
385
  {/* Prompt */}
303
386
  <div className="img-studio-field">
304
387
  <label className="img-studio-label" htmlFor="img-studio-prompt">
305
- Prompt
306
- <span className="img-studio-label-hint">Describe the image you want</span>
388
+ {inputImage ? 'Editing Instruction Prompt' : 'Prompt'}
389
+ <span className="img-studio-label-hint">{inputImage ? 'Describe what to edit or remove' : 'Describe the image you want'}</span>
307
390
  </label>
308
391
  <textarea
309
392
  id="img-studio-prompt"
@@ -7,6 +7,7 @@ import {
7
7
  getRecentInteractions,
8
8
  saveSystemInteraction,
9
9
  getRuntimeStatus,
10
+ setActiveModel,
10
11
  listChatSessions,
11
12
  listSavedPictures,
12
13
  saveInteractionAgentActivity,
@@ -25,6 +26,7 @@ import ChatPanel from './ChatPanel'
25
26
  import DashboardSidebar, { type DashboardView } from './DashboardSidebar'
26
27
  import PicturesLibrary from './PicturesLibrary'
27
28
  import ImageStudioPanel from './ImageStudioPanel'
29
+ import VeoStudioPanel from './VeoStudioPanel'
28
30
  import {
29
31
  errorMessage,
30
32
  readImage,
@@ -32,6 +34,8 @@ import {
32
34
  createTrimmedImagePreview,
33
35
  applyThemeStyles,
34
36
  } from '../../shared/utils/ui'
37
+ import { executeSlashCommand } from '../../shared/utils/slashCommandProcessor'
38
+
35
39
 
36
40
  const DEFAULT_CONFIG = {
37
41
  theme: 'dark',
@@ -84,6 +88,7 @@ export default function MintDashboard() {
84
88
  const [sending, setSending] = useState(false)
85
89
  const [sendingMessage, setSendingMessage] = useState('')
86
90
  const [sendingImageCount, setSendingImageCount] = useState(0)
91
+ const [sendingVideoCount, setSendingVideoCount] = useState(0)
87
92
  const [streamedReply, setStreamedReply] = useState('')
88
93
  const [streamedResponse, setStreamedResponse] = useState<ChatResponse | null>(null)
89
94
  const [streamingConversationId, setStreamingConversationId] = useState<string | null>(null)
@@ -92,6 +97,7 @@ export default function MintDashboard() {
92
97
  const [thinkingExpanded, setThinkingExpanded] = useState<Record<string, boolean>>({})
93
98
  const liveThinkingOpenRef = useRef(true)
94
99
  const [imageAttachments, setImageAttachments] = useState<Array<{ dataUri: string; name: string; previewDataUri?: string }>>([])
100
+ const [videoAttachments, setVideoAttachments] = useState<Array<{ dataUri: string; name: string }>>([])
95
101
  const [documentAttachment, setDocumentAttachment] = useState<DocumentAttachment | null>(null)
96
102
  const [pendingApproval, setPendingApproval] = useState<any | null>(null)
97
103
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('mint:sidebar-collapsed') === 'true')
@@ -225,10 +231,25 @@ export default function MintDashboard() {
225
231
  if (failure?.status === 'rejected') setError(errorMessage(failure.reason))
226
232
  setDashboardDataReady(true)
227
233
  })
234
+ const handleWindowFocus = () => {
235
+ getRuntimeStatus().then(setStatus).catch(() => {})
236
+ window.settingsApi?.getSettings?.().then((loaded: any) => {
237
+ if (loaded) {
238
+ setSettingsConfig(loaded)
239
+ applyThemeStyles(loaded)
240
+ }
241
+ }).catch(() => {})
242
+ }
243
+ window.addEventListener('focus', handleWindowFocus)
244
+
228
245
  window.api?.onSettingsChanged?.((loaded: any) => {
229
246
  setSettingsConfig(loaded)
230
247
  applyThemeStyles(loaded)
248
+ getRuntimeStatus().then(setStatus).catch(() => {})
231
249
  })
250
+ return () => {
251
+ window.removeEventListener('focus', handleWindowFocus)
252
+ }
232
253
  }, [])
233
254
 
234
255
  useEffect(() => {
@@ -304,6 +325,7 @@ export default function MintDashboard() {
304
325
  promptText: string,
305
326
  options: {
306
327
  imageAttachments?: Array<{ dataUri: string; name: string; previewDataUri?: string }>
328
+ videoAttachments?: Array<{ dataUri: string; name: string }>
307
329
  audioDataUri?: string | null
308
330
  documentAttachment?: DocumentAttachment | null
309
331
  systemInstruction?: string
@@ -312,14 +334,17 @@ export default function MintDashboard() {
312
334
  ) {
313
335
  if (sending) return
314
336
  const outgoingImages = options.imageAttachments ?? []
337
+ const outgoingVideos = options.videoAttachments ?? []
315
338
  const outgoingDocument = options.documentAttachment ?? null
316
339
  const shouldUseAgentMode = agentMode || promptText.toLowerCase().startsWith('search web:')
317
340
  const outgoingImage = outgoingImages.map((img) => img.dataUri).join(' ')
341
+ const outgoingVideo = outgoingVideos.map((vid) => vid.dataUri).join(' ')
318
342
  const outgoingImageCount = outgoingImages.length
319
343
  setSending(true)
320
344
  setStreamingConversationId(conversationId)
321
345
  setSendingMessage(promptText)
322
346
  setSendingImageCount(outgoingImageCount)
347
+ setSendingVideoCount(outgoingVideos.length)
323
348
  setError('')
324
349
  setStreamedReply('')
325
350
  setStreamedResponse(null)
@@ -330,6 +355,7 @@ export default function MintDashboard() {
330
355
  if (options.clearComposer) {
331
356
  setMessage('')
332
357
  setImageAttachments([])
358
+ setVideoAttachments([])
333
359
  setDocumentAttachment(null)
334
360
  }
335
361
 
@@ -339,6 +365,7 @@ export default function MintDashboard() {
339
365
  (chunk) => setStreamedReply((current) => `${current}${chunk}`),
340
366
  outgoingImage,
341
367
  options.audioDataUri ?? null,
368
+ outgoingVideo,
342
369
  options.systemInstruction ?? '',
343
370
  (progress) => {
344
371
  progressSnapshot.push(progress)
@@ -379,6 +406,7 @@ export default function MintDashboard() {
379
406
  setAgentActivitySnapshots((current) => mergeActivitySnapshots(current, enrichedHistory))
380
407
  await refreshChatSessions()
381
408
  await refreshPictures()
409
+ getRuntimeStatus().then(setStatus).catch(() => {})
382
410
  setStreamedReply('')
383
411
  setStreamedResponse(null)
384
412
  } catch (reason) {
@@ -388,6 +416,7 @@ export default function MintDashboard() {
388
416
  setStreamingConversationId(null)
389
417
  setSendingMessage('')
390
418
  setSendingImageCount(0)
419
+ setSendingVideoCount(0)
391
420
  }
392
421
  }
393
422
 
@@ -395,12 +424,77 @@ export default function MintDashboard() {
395
424
  event.preventDefault()
396
425
  const trimmed = message.trim()
397
426
  const currentImages = imageAttachments
427
+ const currentVideos = videoAttachments
398
428
  const currentDocument = documentAttachment
399
- const hasAttachments = currentImages.length > 0 || Boolean(currentDocument)
429
+ const hasAttachments = currentImages.length > 0 || currentVideos.length > 0 || Boolean(currentDocument)
400
430
  if ((!trimmed && !hasAttachments) || sending) return
401
- const promptText = trimmed || (currentImages.length > 1 ? 'Describe these images.' : currentImages.length === 1 ? 'Describe this image.' : 'Summarize this document.')
431
+
432
+ if (trimmed.startsWith('/')) {
433
+ const activeP = status?.activeProvider || ''
434
+ const activeM = settingsConfig?.model || ''
435
+ const slashResult = executeSlashCommand(trimmed, {
436
+ activeProvider: activeP,
437
+ activeModel: activeM,
438
+ availableProviders: status?.availableProviders || [],
439
+ interactionsCount: interactions.length,
440
+ fastMode: settingsConfig?.fastMode ?? false,
441
+ multiAgent: settingsConfig?.multiAgent ?? false,
442
+ })
443
+
444
+ if (slashResult.handled) {
445
+ setMessage('')
446
+ setImageAttachments([])
447
+ setVideoAttachments([])
448
+ setDocumentAttachment(null)
449
+
450
+ if (slashResult.action === 'set_agent_mode') {
451
+ setAgentMode(true)
452
+ if (slashResult.payload?.prompt) {
453
+ await sendPrompt(slashResult.payload.prompt, { clearComposer: true })
454
+ }
455
+ return
456
+ }
457
+
458
+ if (slashResult.action === 'open_image_picker') {
459
+ document.getElementById('vision-file-input')?.click()
460
+ return
461
+ } else if (slashResult.action === 'open_plugins') {
462
+ setView('settings')
463
+ return
464
+ } else if (slashResult.action === 'generate_veo') {
465
+ setView('veo_studio')
466
+ return
467
+ } else if (slashResult.action === 'set_provider_model' && slashResult.payload?.target) {
468
+ const target = slashResult.payload.target
469
+ if (status?.availableProviders.includes(target)) {
470
+ setActiveModel(target).then(() => getRuntimeStatus().then(setStatus)).catch(() => {})
471
+ }
472
+ }
473
+
474
+ if (slashResult.systemText) {
475
+ const systemMsg = {
476
+ id: Date.now(),
477
+ userText: trimmed,
478
+ aiText: slashResult.systemText,
479
+ createdAt: new Date().toISOString(),
480
+ provider: 'system',
481
+ model: 'mint-cli',
482
+ }
483
+ setInteractions((prev) => [...prev, systemMsg])
484
+ saveSystemInteraction(conversationId, trimmed, slashResult.systemText || '', 'system', 'mint-cli').catch(() => {})
485
+ }
486
+ return
487
+ }
488
+ }
489
+
490
+ const promptText = trimmed || (
491
+ currentImages.length > 0 ? (currentImages.length > 1 ? 'Describe these images.' : 'Describe this image.') :
492
+ currentVideos.length > 0 ? (currentVideos.length > 1 ? 'Describe these videos.' : 'Describe this video.') :
493
+ 'Summarize this document.'
494
+ )
402
495
  await sendPrompt(promptText, {
403
496
  imageAttachments: currentImages,
497
+ videoAttachments: currentVideos,
404
498
  documentAttachment: currentDocument,
405
499
  clearComposer: true,
406
500
  })
@@ -430,6 +524,28 @@ export default function MintDashboard() {
430
524
  event.target.value = ''
431
525
  }
432
526
  }
527
+ const MAX_VIDEO_BYTES = 25 * 1024 * 1024 // 25 MB
528
+ async function selectVideo(event: ChangeEvent<HTMLInputElement>) {
529
+ const file = event.target.files?.[0]
530
+ if (!file) return
531
+ try {
532
+ if (file.size > MAX_VIDEO_BYTES) {
533
+ setError(`Video is too large. Maximum allowed is ${Math.round(MAX_VIDEO_BYTES / (1024 * 1024))} MB.`)
534
+ return
535
+ }
536
+ const dataUri = await new Promise<string>((resolve, reject) => {
537
+ const reader = new FileReader()
538
+ reader.onload = () => resolve(reader.result as string)
539
+ reader.onerror = reject
540
+ reader.readAsDataURL(file)
541
+ })
542
+ setVideoAttachments((current) => [...current, { dataUri, name: file.name }])
543
+ } catch (reason) {
544
+ setError(errorMessage(reason))
545
+ } finally {
546
+ event.target.value = ''
547
+ }
548
+ }
433
549
 
434
550
  function pasteImage(clipboardData: DataTransfer) {
435
551
  let file: File | null = null
@@ -606,6 +722,11 @@ export default function MintDashboard() {
606
722
  else if (provider === 'huggingface') providerName = 'HF'
607
723
  else if (provider === 'local_openai') providerName = 'Local'
608
724
  else if (provider === 'ollama') providerName = 'Ollama'
725
+ else if (provider.startsWith('custom:')) {
726
+ const id = provider.replace(/^custom:/, '')
727
+ const cp = (settingsConfig?.customProviders ?? []).find((p: any) => p.id === id)
728
+ providerName = cp?.displayName || id
729
+ }
609
730
 
610
731
  if (providerName && providerName === provider) {
611
732
  providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
@@ -623,13 +744,21 @@ export default function MintDashboard() {
623
744
  case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
624
745
  case 'local_openai': return config.localModelName || 'llama3'
625
746
  case 'ollama': return config.ollamaModel || 'llama3:latest'
626
- default: return ''
747
+ default: {
748
+ if (provider.startsWith('custom:')) {
749
+ const id = provider.replace(/^custom:/, '')
750
+ const cp = (config.customProviders ?? []).find((p: any) => p.id === id)
751
+ return (config.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
752
+ }
753
+ return ''
754
+ }
627
755
  }
628
756
  }
629
757
 
630
758
  async function changeProvider(provider: string) {
631
759
  try {
632
760
  const config = await window.settingsApi.getSettings()
761
+ if (config.aiProvider === provider) return
633
762
  config.aiProvider = provider
634
763
  await window.settingsApi.saveSettings(config)
635
764
  setSettingsConfig(config)
@@ -638,7 +767,7 @@ export default function MintDashboard() {
638
767
  // Record system event in chat history
639
768
  const activeModel = getActiveModelName(config, provider)
640
769
  const displayName = formatProviderChangeText(provider, activeModel)
641
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
770
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
642
771
  await refreshHistory()
643
772
  } catch (reason) {
644
773
  setError(errorMessage(reason))
@@ -649,6 +778,8 @@ export default function MintDashboard() {
649
778
  try {
650
779
  const config = await window.settingsApi.getSettings()
651
780
  const provider = config.aiProvider
781
+ const currentModel = getActiveModelName(config, provider)
782
+ if (currentModel === modelName) return
652
783
  if (provider === 'gemini') {
653
784
  config.geminiModel = modelName
654
785
  } else if (provider === 'openai') {
@@ -665,6 +796,12 @@ export default function MintDashboard() {
665
796
  config.localModelName = modelName
666
797
  } else if (provider === 'ollama') {
667
798
  config.ollamaModel = modelName
799
+ } else if (provider.startsWith('custom:')) {
800
+ const id = provider.replace(/^custom:/, '')
801
+ config.customModelSelections = {
802
+ ...(config.customModelSelections ?? {}),
803
+ [id]: modelName
804
+ }
668
805
  }
669
806
  await window.settingsApi.saveSettings(config)
670
807
  setSettingsConfig(config)
@@ -672,7 +809,7 @@ export default function MintDashboard() {
672
809
 
673
810
  // Record system event in chat history
674
811
  const displayName = formatProviderChangeText(provider, modelName)
675
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
812
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
676
813
  await refreshHistory()
677
814
  } catch (reason) {
678
815
  setError(errorMessage(reason))
@@ -724,11 +861,13 @@ export default function MintDashboard() {
724
861
  streamedReply={streamingConversationId === conversationId ? streamedReply : ''}
725
862
  streamedResponse={streamingConversationId === conversationId ? streamedResponse : null}
726
863
  agentProgress={streamingConversationId === conversationId ? agentProgress : []}
864
+ sendingVideoCount={streamingConversationId === conversationId ? sendingVideoCount : 0}
727
865
  agentActivitySnapshots={agentActivitySnapshots}
728
866
  thinkingExpanded={thinkingExpanded}
729
867
  onThinkingExpandedChange={handleThinkingExpandedChange}
730
868
  message={message}
731
869
  imageAttachments={imageAttachments}
870
+ videoAttachments={videoAttachments}
732
871
  documentName={documentAttachment?.filename ?? ''}
733
872
  pendingApproval={streamingConversationId === conversationId ? pendingApproval : null}
734
873
  smartContext={smartContext}
@@ -738,6 +877,7 @@ export default function MintDashboard() {
738
877
  welcomeInteraction={MOCK_WELCOME_INTERACTION}
739
878
  onSubmit={handleSubmit}
740
879
  onSelectImage={selectImage}
880
+ onSelectVideo={selectVideo}
741
881
  onSelectDocument={selectDocument}
742
882
  onPasteImage={pasteImage}
743
883
  onSetMessage={setMessage}
@@ -745,6 +885,9 @@ export default function MintDashboard() {
745
885
  onRemoveImage={(idx: number) => {
746
886
  setImageAttachments((current) => current.filter((_, i) => i !== idx))
747
887
  }}
888
+ onRemoveVideo={(idx: number) => {
889
+ setVideoAttachments((current) => current.filter((_, i) => i !== idx))
890
+ }}
748
891
  onRemoveDocument={() => setDocumentAttachment(null)}
749
892
  onStartWebSearch={startWebSearch}
750
893
  onCaptureScreen={captureScreen}
@@ -768,6 +911,14 @@ export default function MintDashboard() {
768
911
  }}
769
912
  onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
770
913
  />
914
+ <VeoStudioPanel
915
+ view={view}
916
+ onSendToChat={(imgPrompt) => {
917
+ setView('chat')
918
+ setMessage(imgPrompt)
919
+ }}
920
+ onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
921
+ />
771
922
  </div>
772
923
  <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
773
924
  <div className="startup-loading-content">