@pheem49/mint 1.9.0 → 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 (90) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +285 -140
  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 +287 -43
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/interactive.rs +2888 -0
  10. package/crates/mint-cli/src/main.rs +336 -2854
  11. package/crates/mint-cli/src/markdown.rs +294 -0
  12. package/crates/mint-cli/src/onboard.rs +44 -1
  13. package/crates/mint-cli/src/updater.rs +48 -6
  14. package/crates/mint-core/Cargo.toml +1 -0
  15. package/crates/mint-core/src/agent_loop.rs +1 -0
  16. package/crates/mint-core/src/api_server.rs +376 -13
  17. package/crates/mint-core/src/browser.rs +629 -101
  18. package/crates/mint-core/src/channels.rs +105 -6
  19. package/crates/mint-core/src/chat.rs +52 -4
  20. package/crates/mint-core/src/code_tools.rs +29 -11
  21. package/crates/mint-core/src/config.rs +118 -0
  22. package/crates/mint-core/src/image_gen.rs +294 -33
  23. package/crates/mint-core/src/lib.rs +5 -2
  24. package/crates/mint-core/src/mcp.rs +119 -6
  25. package/crates/mint-core/src/orchestration.rs +133 -11
  26. package/crates/mint-core/src/pictures.rs +64 -1
  27. package/crates/mint-core/src/skills.rs +82 -6
  28. package/crates/mint-core/src/video_gen.rs +235 -0
  29. package/package.json +8 -2
  30. package/shared-modules/luajit/luajit.json +2 -2
  31. package/skills-lock.json +41 -0
  32. package/src/renderer/index-web.html +1 -1
  33. package/src/renderer/index.html +1 -1
  34. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  35. package/src/renderer/shared/platform.ts +3 -0
  36. package/src/renderer/shared/types.ts +4 -0
  37. package/src/renderer/shared/utils/approval.tsx +86 -7
  38. package/src/renderer/shared/utils/markdown.tsx +217 -88
  39. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  40. package/src/renderer/src/components/ChatPanel.tsx +461 -64
  41. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  42. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  43. package/src/renderer/src/components/MintDashboard.tsx +150 -5
  44. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  45. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  46. package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
  47. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  48. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  49. package/src/renderer/src/components/SettingsWindow.tsx +38 -2
  50. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  51. package/src/renderer/src/css/image-studio.css +106 -0
  52. package/src/renderer/src/css/settings.css +158 -0
  53. package/src/renderer/src/css/styles.css +352 -1
  54. package/src/renderer/src/css/veo-studio.css +626 -0
  55. package/src/renderer/src/index.css +1 -0
  56. package/src/renderer/src/tauri.ts +144 -17
  57. package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
  58. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  59. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  60. package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
  61. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  62. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  63. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
  64. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  65. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  66. package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
  67. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  68. package/src/renderer/src-web/css/image-studio.css +106 -0
  69. package/src/renderer/src-web/css/settings.css +158 -0
  70. package/src/renderer/src-web/css/styles.css +353 -1
  71. package/src/renderer/src-web/css/veo-studio.css +650 -0
  72. package/src/renderer/src-web/index.css +1 -0
  73. package/src/renderer/src-web/tauri.ts +135 -16
  74. package/src-tauri/src/lib.rs +114 -8
  75. package/src-tauri/src/webhooks.rs +45 -31
  76. package/src-tauri/src/workflows.rs +1 -0
  77. package/src-tauri/tauri.conf.json +1 -1
  78. package/temp_gridgeist/AGENTS.md +8 -0
  79. package/temp_gridgeist/CHANGELOG.md +130 -0
  80. package/temp_gridgeist/LICENSE +21 -0
  81. package/temp_gridgeist/README.md +323 -0
  82. package/temp_gridgeist/README.th.md +325 -0
  83. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  84. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  85. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  86. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  87. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  88. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  89. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  90. 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
@@ -642,6 +758,7 @@ export default function MintDashboard() {
642
758
  async function changeProvider(provider: string) {
643
759
  try {
644
760
  const config = await window.settingsApi.getSettings()
761
+ if (config.aiProvider === provider) return
645
762
  config.aiProvider = provider
646
763
  await window.settingsApi.saveSettings(config)
647
764
  setSettingsConfig(config)
@@ -650,7 +767,7 @@ export default function MintDashboard() {
650
767
  // Record system event in chat history
651
768
  const activeModel = getActiveModelName(config, provider)
652
769
  const displayName = formatProviderChangeText(provider, activeModel)
653
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
770
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
654
771
  await refreshHistory()
655
772
  } catch (reason) {
656
773
  setError(errorMessage(reason))
@@ -661,6 +778,8 @@ export default function MintDashboard() {
661
778
  try {
662
779
  const config = await window.settingsApi.getSettings()
663
780
  const provider = config.aiProvider
781
+ const currentModel = getActiveModelName(config, provider)
782
+ if (currentModel === modelName) return
664
783
  if (provider === 'gemini') {
665
784
  config.geminiModel = modelName
666
785
  } else if (provider === 'openai') {
@@ -690,7 +809,7 @@ export default function MintDashboard() {
690
809
 
691
810
  // Record system event in chat history
692
811
  const displayName = formatProviderChangeText(provider, modelName)
693
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
812
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
694
813
  await refreshHistory()
695
814
  } catch (reason) {
696
815
  setError(errorMessage(reason))
@@ -742,11 +861,13 @@ export default function MintDashboard() {
742
861
  streamedReply={streamingConversationId === conversationId ? streamedReply : ''}
743
862
  streamedResponse={streamingConversationId === conversationId ? streamedResponse : null}
744
863
  agentProgress={streamingConversationId === conversationId ? agentProgress : []}
864
+ sendingVideoCount={streamingConversationId === conversationId ? sendingVideoCount : 0}
745
865
  agentActivitySnapshots={agentActivitySnapshots}
746
866
  thinkingExpanded={thinkingExpanded}
747
867
  onThinkingExpandedChange={handleThinkingExpandedChange}
748
868
  message={message}
749
869
  imageAttachments={imageAttachments}
870
+ videoAttachments={videoAttachments}
750
871
  documentName={documentAttachment?.filename ?? ''}
751
872
  pendingApproval={streamingConversationId === conversationId ? pendingApproval : null}
752
873
  smartContext={smartContext}
@@ -756,6 +877,7 @@ export default function MintDashboard() {
756
877
  welcomeInteraction={MOCK_WELCOME_INTERACTION}
757
878
  onSubmit={handleSubmit}
758
879
  onSelectImage={selectImage}
880
+ onSelectVideo={selectVideo}
759
881
  onSelectDocument={selectDocument}
760
882
  onPasteImage={pasteImage}
761
883
  onSetMessage={setMessage}
@@ -763,6 +885,9 @@ export default function MintDashboard() {
763
885
  onRemoveImage={(idx: number) => {
764
886
  setImageAttachments((current) => current.filter((_, i) => i !== idx))
765
887
  }}
888
+ onRemoveVideo={(idx: number) => {
889
+ setVideoAttachments((current) => current.filter((_, i) => i !== idx))
890
+ }}
766
891
  onRemoveDocument={() => setDocumentAttachment(null)}
767
892
  onStartWebSearch={startWebSearch}
768
893
  onCaptureScreen={captureScreen}
@@ -786,6 +911,14 @@ export default function MintDashboard() {
786
911
  }}
787
912
  onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
788
913
  />
914
+ <VeoStudioPanel
915
+ view={view}
916
+ onSendToChat={(imgPrompt) => {
917
+ setView('chat')
918
+ setMessage(imgPrompt)
919
+ }}
920
+ onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
921
+ />
789
922
  </div>
790
923
  <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
791
924
  <div className="startup-loading-content">