@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, useEffect, useRef } from 'react'
2
2
 
3
- export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspace' | 'imagine' | 'workflows'
3
+ export type DashboardView = 'chat' | 'pictures' | 'model' | 'workspace' | 'imagine' | 'workflows' | 'veo'
4
4
 
5
5
  interface ChatSessionItem {
6
6
  id: string
@@ -181,7 +181,7 @@ export default function DashboardSidebar({
181
181
  </button>
182
182
 
183
183
  <div className="sidebar-more-container" ref={moreContainerRef}>
184
- <button className={`sidebar-top-action ${isMoreOpen || view === 'imagine' || view === 'workflows' ? 'is-active' : ''}`} onClick={() => setIsMoreOpen(!isMoreOpen)}>
184
+ <button className={`sidebar-top-action ${isMoreOpen || view === 'imagine' || view === 'workflows' || view === 'veo' ? 'is-active' : ''}`} onClick={() => setIsMoreOpen(!isMoreOpen)}>
185
185
  <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
186
186
  <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">
187
187
  <circle cx="12" cy="12" r="1.5"></circle>
@@ -214,6 +214,15 @@ export default function DashboardSidebar({
214
214
  </span>
215
215
  <span>Workflow (Beta)</span>
216
216
  </button>
217
+ <button className={`popover-item ${view === 'veo' ? 'active' : ''}`} onClick={() => { onSetView('veo'); setIsMoreOpen(false); }}>
218
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
219
+ <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">
220
+ <polygon points="23 7 16 12 23 17 23 7"></polygon>
221
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
222
+ </svg>
223
+ </span>
224
+ <span>Veo Studio</span>
225
+ </button>
217
226
  </div>
218
227
  )}
219
228
  </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
  selectWorkspaceDirectory,
@@ -27,6 +28,7 @@ import {
27
28
  import ChatPanel from './ChatPanel'
28
29
  import DashboardSidebar, { type DashboardView } from './DashboardSidebar'
29
30
  import ImageStudioPanel from './ImageStudioPanel'
31
+ import VeoStudioPanel from './VeoStudioPanel'
30
32
  import ModelPanel from './ModelPanel'
31
33
  import type { ModelInteraction } from './ModelPanel'
32
34
  import PicturesLibrary from './PicturesLibrary'
@@ -39,6 +41,8 @@ import {
39
41
  createTrimmedImagePreview,
40
42
  applyThemeStyles,
41
43
  } from '../../shared/utils/ui'
44
+ import { executeSlashCommand } from '../../shared/utils/slashCommandProcessor'
45
+
42
46
 
43
47
  const EXPRESSIONS = [
44
48
  "Default",
@@ -108,6 +112,7 @@ export default function MintDashboard() {
108
112
  const [sending, setSending] = useState(false)
109
113
  const [sendingMessage, setSendingMessage] = useState('')
110
114
  const [sendingImageCount, setSendingImageCount] = useState(0)
115
+ const [sendingVideoCount, setSendingVideoCount] = useState(0)
111
116
  const [streamedReply, setStreamedReply] = useState('')
112
117
  const [streamedResponse, setStreamedResponse] = useState<ChatResponse | null>(null)
113
118
  const [streamingConversationId, setStreamingConversationId] = useState<string | null>(null)
@@ -116,6 +121,7 @@ export default function MintDashboard() {
116
121
  const [thinkingExpanded, setThinkingExpanded] = useState<Record<string, boolean>>({})
117
122
  const liveThinkingOpenRef = useRef(true)
118
123
  const [imageAttachments, setImageAttachments] = useState<Array<{ dataUri: string; name: string; previewDataUri?: string }>>([])
124
+ const [videoAttachments, setVideoAttachments] = useState<Array<{ dataUri: string; name: string }>>([])
119
125
  const [documentAttachment, setDocumentAttachment] = useState<DocumentAttachment | null>(null)
120
126
  const [pendingApproval, setPendingApproval] = useState<any | null>(null)
121
127
  const [sessionAutoApproved, setSessionAutoApproved] = useState(false)
@@ -280,9 +286,21 @@ export default function MintDashboard() {
280
286
  setImageAttachments((current) => [...current, { dataUri: image, previewDataUri, name: 'Screen capture' }])
281
287
  })
282
288
  })
289
+ const handleWindowFocus = () => {
290
+ getRuntimeStatus().then(setStatus).catch(() => {})
291
+ window.settingsApi?.getSettings?.().then((loaded: any) => {
292
+ if (loaded) {
293
+ setSettingsConfig(loaded)
294
+ applyThemeStyles(loaded)
295
+ }
296
+ }).catch(() => {})
297
+ }
298
+ window.addEventListener('focus', handleWindowFocus)
299
+
283
300
  window.api?.onSettingsChanged?.((loaded: any) => {
284
301
  setSettingsConfig(loaded)
285
302
  applyThemeStyles(loaded)
303
+ getRuntimeStatus().then(setStatus).catch(() => {})
286
304
  })
287
305
 
288
306
  const unlistenPromise = listen<any>('tool-approval-requested', (event) => {
@@ -298,6 +316,7 @@ export default function MintDashboard() {
298
316
  setProactiveSuggestion(suggestion)
299
317
  })
300
318
  return () => {
319
+ window.removeEventListener('focus', handleWindowFocus)
301
320
  unlistenPromise?.then?.((unlisten) => unlisten?.())
302
321
  unlistenSpotlight?.then?.((unlisten) => unlisten?.())
303
322
  unlistenVision?.then?.((unlisten) => unlisten?.())
@@ -407,6 +426,7 @@ export default function MintDashboard() {
407
426
  setStreamingConversationId(null)
408
427
  setSendingMessage('')
409
428
  setSendingImageCount(0)
429
+ setSendingVideoCount(0)
410
430
  }
411
431
  }
412
432
 
@@ -414,6 +434,7 @@ export default function MintDashboard() {
414
434
  promptText: string,
415
435
  options: {
416
436
  imageAttachments?: Array<{ dataUri: string; name: string; previewDataUri?: string }>
437
+ videoAttachments?: Array<{ dataUri: string; name: string }>
417
438
  audioDataUri?: string | null
418
439
  documentAttachment?: DocumentAttachment | null
419
440
  systemInstruction?: string
@@ -422,14 +443,17 @@ export default function MintDashboard() {
422
443
  ) {
423
444
  if (sending) return
424
445
  const outgoingImages = options.imageAttachments ?? []
446
+ const outgoingVideos = options.videoAttachments ?? []
425
447
  const outgoingDocument = options.documentAttachment ?? null
426
448
  const shouldUseAgentMode = agentMode || promptText.toLowerCase().startsWith('search web:')
427
449
  const outgoingImage = outgoingImages.map((img) => img.dataUri).join(' ')
450
+ const outgoingVideo = outgoingVideos.map((vid) => vid.dataUri).join(' ')
428
451
  const outgoingImageCount = outgoingImages.length
429
452
  setSending(true)
430
453
  setStreamingConversationId(conversationId)
431
454
  setSendingMessage(promptText)
432
455
  setSendingImageCount(outgoingImageCount)
456
+ setSendingVideoCount(outgoingVideos.length)
433
457
  setError('')
434
458
  setStreamedReply('')
435
459
  setStreamedResponse(null)
@@ -440,6 +464,7 @@ export default function MintDashboard() {
440
464
  if (options.clearComposer) {
441
465
  setMessage('')
442
466
  setImageAttachments([])
467
+ setVideoAttachments([])
443
468
  setDocumentAttachment(null)
444
469
  }
445
470
 
@@ -449,6 +474,7 @@ export default function MintDashboard() {
449
474
  (chunk) => setStreamedReply((current) => `${current}${chunk}`),
450
475
  outgoingImage,
451
476
  options.audioDataUri ?? null,
477
+ outgoingVideo,
452
478
  options.systemInstruction ?? '',
453
479
  (progress) => {
454
480
  progressSnapshot.push(progress)
@@ -489,6 +515,7 @@ export default function MintDashboard() {
489
515
  setAgentActivitySnapshots((current) => mergeActivitySnapshots(current, enrichedHistory))
490
516
  await refreshChatSessions()
491
517
  await refreshPictures()
518
+ getRuntimeStatus().then(setStatus).catch(() => {})
492
519
  setStreamedReply('')
493
520
  setStreamedResponse(null)
494
521
  } catch (reason) {
@@ -498,6 +525,7 @@ export default function MintDashboard() {
498
525
  setStreamingConversationId(null)
499
526
  setSendingMessage('')
500
527
  setSendingImageCount(0)
528
+ setSendingVideoCount(0)
501
529
  }
502
530
  }
503
531
 
@@ -516,12 +544,91 @@ export default function MintDashboard() {
516
544
  event.preventDefault()
517
545
  const trimmed = message.trim()
518
546
  const currentImages = imageAttachments
547
+ const currentVideos = videoAttachments
519
548
  const currentDocument = documentAttachment
520
- const hasAttachments = currentImages.length > 0 || Boolean(currentDocument)
549
+ const hasAttachments = currentImages.length > 0 || currentVideos.length > 0 || Boolean(currentDocument)
521
550
  if ((!trimmed && !hasAttachments) || sending) return
522
- const promptText = trimmed || (currentImages.length > 1 ? 'Describe these images.' : currentImages.length === 1 ? 'Describe this image.' : 'Summarize this document.')
551
+
552
+ if (trimmed.startsWith('/')) {
553
+ const activeP = status?.activeProvider || ''
554
+ const activeM = settingsConfig?.model || ''
555
+ const slashResult = executeSlashCommand(trimmed, {
556
+ activeProvider: activeP,
557
+ activeModel: activeM,
558
+ availableProviders: status?.availableProviders || [],
559
+ workspacePath,
560
+ interactionsCount: interactions.length,
561
+ fastMode: settingsConfig?.fastMode ?? false,
562
+ multiAgent: settingsConfig?.multiAgent ?? false,
563
+ })
564
+
565
+ if (slashResult.handled) {
566
+ setMessage('')
567
+ setImageAttachments([])
568
+ setVideoAttachments([])
569
+ setDocumentAttachment(null)
570
+
571
+ if (slashResult.action === 'set_agent_mode') {
572
+ setAgentMode(true)
573
+ if (slashResult.payload?.prompt) {
574
+ await sendPrompt(slashResult.payload.prompt, { clearComposer: true })
575
+ }
576
+ return
577
+ }
578
+
579
+ if (slashResult.action === 'change_workspace') {
580
+ if (slashResult.payload?.path) {
581
+ setWorkspacePath(slashResult.payload.path)
582
+ } else {
583
+ selectWorkspaceDirectory().then((res) => {
584
+ if (res) setWorkspacePath(res)
585
+ }).catch(() => {})
586
+ }
587
+ } else if (slashResult.action === 'open_image_picker') {
588
+ document.getElementById('vision-file-input')?.click()
589
+ return
590
+ } else if (slashResult.action === 'paste_image') {
591
+ readClipboardImage().then((uri) => {
592
+ if (uri) setImageAttachments((curr) => [...curr, { dataUri: uri, name: 'Clipboard Image' }])
593
+ }).catch(() => {})
594
+ return
595
+ } else if (slashResult.action === 'open_plugins') {
596
+ setView('settings')
597
+ return
598
+ } else if (slashResult.action === 'generate_veo') {
599
+ setView('veo_studio')
600
+ return
601
+ } else if (slashResult.action === 'set_provider_model' && slashResult.payload?.target) {
602
+ const target = slashResult.payload.target
603
+ if (status?.availableProviders.includes(target)) {
604
+ setActiveModel(target).then(() => getRuntimeStatus().then(setStatus)).catch(() => {})
605
+ }
606
+ }
607
+
608
+ if (slashResult.systemText) {
609
+ const systemMsg = {
610
+ id: Date.now(),
611
+ userText: trimmed,
612
+ aiText: slashResult.systemText,
613
+ createdAt: new Date().toISOString(),
614
+ provider: 'system',
615
+ model: 'mint-cli',
616
+ }
617
+ setInteractions((prev) => [...prev, systemMsg])
618
+ saveSystemInteraction(conversationId, trimmed, slashResult.systemText || '', 'system', 'mint-cli').catch(() => {})
619
+ }
620
+ return
621
+ }
622
+ }
623
+
624
+ const promptText = trimmed || (
625
+ currentImages.length > 0 ? (currentImages.length > 1 ? 'Describe these images.' : 'Describe this image.') :
626
+ currentVideos.length > 0 ? (currentVideos.length > 1 ? 'Describe these videos.' : 'Describe this video.') :
627
+ 'Summarize this document.'
628
+ )
523
629
  await sendPrompt(promptText, {
524
630
  imageAttachments: currentImages,
631
+ videoAttachments: currentVideos,
525
632
  documentAttachment: currentDocument,
526
633
  clearComposer: true,
527
634
  })
@@ -551,6 +658,28 @@ export default function MintDashboard() {
551
658
  event.target.value = ''
552
659
  }
553
660
  }
661
+ const MAX_VIDEO_BYTES = 25 * 1024 * 1024 // 25 MB
662
+ async function selectVideo(event: ChangeEvent<HTMLInputElement>) {
663
+ const file = event.target.files?.[0]
664
+ if (!file) return
665
+ try {
666
+ if (file.size > MAX_VIDEO_BYTES) {
667
+ setError(`Video is too large. Maximum allowed is ${Math.round(MAX_VIDEO_BYTES / (1024 * 1024))} MB.`)
668
+ return
669
+ }
670
+ const dataUri = await new Promise<string>((resolve, reject) => {
671
+ const reader = new FileReader()
672
+ reader.onload = () => resolve(reader.result as string)
673
+ reader.onerror = reject
674
+ reader.readAsDataURL(file)
675
+ })
676
+ setVideoAttachments((current) => [...current, { dataUri, name: file.name }])
677
+ } catch (reason) {
678
+ setError(errorMessage(reason))
679
+ } finally {
680
+ event.target.value = ''
681
+ }
682
+ }
554
683
 
555
684
  function pasteImage(clipboardData: DataTransfer) {
556
685
  let file: File | null = null
@@ -797,6 +926,7 @@ export default function MintDashboard() {
797
926
  async function changeProvider(provider: string) {
798
927
  try {
799
928
  const config = await window.settingsApi.getSettings()
929
+ if (config.aiProvider === provider) return
800
930
  config.aiProvider = provider
801
931
  await window.settingsApi.saveSettings(config)
802
932
  setSettingsConfig(config)
@@ -805,7 +935,7 @@ export default function MintDashboard() {
805
935
  // Record system event in chat history
806
936
  const activeModel = getActiveModelName(config, provider)
807
937
  const displayName = formatProviderChangeText(provider, activeModel)
808
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
938
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
809
939
  await refreshHistory()
810
940
  } catch (reason) {
811
941
  setError(errorMessage(reason))
@@ -816,6 +946,8 @@ export default function MintDashboard() {
816
946
  try {
817
947
  const config = await window.settingsApi.getSettings()
818
948
  const provider = config.aiProvider
949
+ const currentModel = getActiveModelName(config, provider)
950
+ if (currentModel === modelName) return
819
951
  if (provider === 'gemini') {
820
952
  config.geminiModel = modelName
821
953
  } else if (provider === 'openai') {
@@ -845,7 +977,7 @@ export default function MintDashboard() {
845
977
 
846
978
  // Record system event in chat history
847
979
  const displayName = formatProviderChangeText(provider, modelName)
848
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
980
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
849
981
  await refreshHistory()
850
982
  } catch (reason) {
851
983
  setError(errorMessage(reason))
@@ -968,7 +1100,7 @@ export default function MintDashboard() {
968
1100
  expressionIndex={expressionIndex}
969
1101
  accessoryIndex={accessoryIndex}
970
1102
  isLocked={isLocked}
971
- isActive={modelVisible && view !== 'pictures' && view !== 'workspace' && view !== 'workflows'}
1103
+ isActive={modelVisible && view !== 'pictures' && view !== 'workspace' && view !== 'workflows' && view !== 'imagine' && view !== 'veo'}
972
1104
  layoutPreset={layoutPreset}
973
1105
  sending={sending}
974
1106
  interactionEnabled={interactionEnabled}
@@ -990,11 +1122,13 @@ export default function MintDashboard() {
990
1122
  streamedReply={streamingConversationId === conversationId ? streamedReply : ''}
991
1123
  streamedResponse={streamingConversationId === conversationId ? streamedResponse : null}
992
1124
  agentProgress={streamingConversationId === conversationId ? agentProgress : []}
1125
+ sendingVideoCount={streamingConversationId === conversationId ? sendingVideoCount : 0}
993
1126
  agentActivitySnapshots={agentActivitySnapshots}
994
1127
  thinkingExpanded={thinkingExpanded}
995
1128
  onThinkingExpandedChange={handleThinkingExpandedChange}
996
1129
  message={message}
997
1130
  imageAttachments={imageAttachments}
1131
+ videoAttachments={videoAttachments}
998
1132
  documentName={documentAttachment?.filename ?? ''}
999
1133
  pendingApproval={streamingConversationId === conversationId ? pendingApproval : null}
1000
1134
  smartContext={smartContext}
@@ -1005,6 +1139,7 @@ export default function MintDashboard() {
1005
1139
  welcomeInteraction={MOCK_WELCOME_INTERACTION}
1006
1140
  onSubmit={handleSubmit}
1007
1141
  onSelectImage={selectImage}
1142
+ onSelectVideo={selectVideo}
1008
1143
  onSelectDocument={selectDocument}
1009
1144
  onPasteImage={pasteImage}
1010
1145
  onSetMessage={setMessage}
@@ -1012,6 +1147,9 @@ export default function MintDashboard() {
1012
1147
  onRemoveImage={(idx: number) => {
1013
1148
  setImageAttachments((current) => current.filter((_, i) => i !== idx))
1014
1149
  }}
1150
+ onRemoveVideo={(idx: number) => {
1151
+ setVideoAttachments((current) => current.filter((_, i) => i !== idx))
1152
+ }}
1015
1153
  onRemoveDocument={() => setDocumentAttachment(null)}
1016
1154
  onStartWebSearch={startWebSearch}
1017
1155
  onCaptureScreen={captureScreen}
@@ -1034,6 +1172,13 @@ export default function MintDashboard() {
1034
1172
  setMessage(imgPrompt)
1035
1173
  }}
1036
1174
  />
1175
+ <VeoStudioPanel
1176
+ view={view}
1177
+ onSendToChat={(vidPrompt) => {
1178
+ setView('chat')
1179
+ setMessage(vidPrompt)
1180
+ }}
1181
+ />
1037
1182
  <WorkflowBuilderPanel
1038
1183
  view={view}
1039
1184
  onShowToast={showToast}