@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, 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,
@@ -15,6 +16,7 @@ import {
15
16
  cancelChatMessage,
16
17
  submitToolApproval,
17
18
  listen,
19
+ readClipboardImage,
18
20
  type AgentProgress,
19
21
  type ChatResponse,
20
22
  type ChatSession,
@@ -22,9 +24,11 @@ import {
22
24
  type PictureEntry,
23
25
  type RuntimeStatus,
24
26
  } from '../tauri'
27
+
25
28
  import ChatPanel from './ChatPanel'
26
29
  import DashboardSidebar, { type DashboardView } from './DashboardSidebar'
27
30
  import ImageStudioPanel from './ImageStudioPanel'
31
+ import VeoStudioPanel from './VeoStudioPanel'
28
32
  import ModelPanel from './ModelPanel'
29
33
  import type { ModelInteraction } from './ModelPanel'
30
34
  import PicturesLibrary from './PicturesLibrary'
@@ -37,6 +41,8 @@ import {
37
41
  createTrimmedImagePreview,
38
42
  applyThemeStyles,
39
43
  } from '../../shared/utils/ui'
44
+ import { executeSlashCommand } from '../../shared/utils/slashCommandProcessor'
45
+
40
46
 
41
47
  const EXPRESSIONS = [
42
48
  "Default",
@@ -106,6 +112,7 @@ export default function MintDashboard() {
106
112
  const [sending, setSending] = useState(false)
107
113
  const [sendingMessage, setSendingMessage] = useState('')
108
114
  const [sendingImageCount, setSendingImageCount] = useState(0)
115
+ const [sendingVideoCount, setSendingVideoCount] = useState(0)
109
116
  const [streamedReply, setStreamedReply] = useState('')
110
117
  const [streamedResponse, setStreamedResponse] = useState<ChatResponse | null>(null)
111
118
  const [streamingConversationId, setStreamingConversationId] = useState<string | null>(null)
@@ -114,6 +121,7 @@ export default function MintDashboard() {
114
121
  const [thinkingExpanded, setThinkingExpanded] = useState<Record<string, boolean>>({})
115
122
  const liveThinkingOpenRef = useRef(true)
116
123
  const [imageAttachments, setImageAttachments] = useState<Array<{ dataUri: string; name: string; previewDataUri?: string }>>([])
124
+ const [videoAttachments, setVideoAttachments] = useState<Array<{ dataUri: string; name: string }>>([])
117
125
  const [documentAttachment, setDocumentAttachment] = useState<DocumentAttachment | null>(null)
118
126
  const [pendingApproval, setPendingApproval] = useState<any | null>(null)
119
127
  const [sessionAutoApproved, setSessionAutoApproved] = useState(false)
@@ -138,6 +146,7 @@ export default function MintDashboard() {
138
146
  const [conversationId, setConversationId] = useState(activeConversationId)
139
147
  const [chatSessions, setChatSessions] = useState<ChatSession[]>([])
140
148
  const chatEnd = useRef<HTMLDivElement | null>(null)
149
+ const lastNativePasteTimeRef = useRef(0)
141
150
  const startupReady = (dashboardDataReady && modelReady) || startupTimedOut
142
151
  const [proactiveSuggestion, setProactiveSuggestion] = useState<any>(null)
143
152
 
@@ -277,9 +286,21 @@ export default function MintDashboard() {
277
286
  setImageAttachments((current) => [...current, { dataUri: image, previewDataUri, name: 'Screen capture' }])
278
287
  })
279
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
+
280
300
  window.api?.onSettingsChanged?.((loaded: any) => {
281
301
  setSettingsConfig(loaded)
282
302
  applyThemeStyles(loaded)
303
+ getRuntimeStatus().then(setStatus).catch(() => {})
283
304
  })
284
305
 
285
306
  const unlistenPromise = listen<any>('tool-approval-requested', (event) => {
@@ -295,6 +316,7 @@ export default function MintDashboard() {
295
316
  setProactiveSuggestion(suggestion)
296
317
  })
297
318
  return () => {
319
+ window.removeEventListener('focus', handleWindowFocus)
298
320
  unlistenPromise?.then?.((unlisten) => unlisten?.())
299
321
  unlistenSpotlight?.then?.((unlisten) => unlisten?.())
300
322
  unlistenVision?.then?.((unlisten) => unlisten?.())
@@ -404,6 +426,7 @@ export default function MintDashboard() {
404
426
  setStreamingConversationId(null)
405
427
  setSendingMessage('')
406
428
  setSendingImageCount(0)
429
+ setSendingVideoCount(0)
407
430
  }
408
431
  }
409
432
 
@@ -411,6 +434,7 @@ export default function MintDashboard() {
411
434
  promptText: string,
412
435
  options: {
413
436
  imageAttachments?: Array<{ dataUri: string; name: string; previewDataUri?: string }>
437
+ videoAttachments?: Array<{ dataUri: string; name: string }>
414
438
  audioDataUri?: string | null
415
439
  documentAttachment?: DocumentAttachment | null
416
440
  systemInstruction?: string
@@ -419,14 +443,17 @@ export default function MintDashboard() {
419
443
  ) {
420
444
  if (sending) return
421
445
  const outgoingImages = options.imageAttachments ?? []
446
+ const outgoingVideos = options.videoAttachments ?? []
422
447
  const outgoingDocument = options.documentAttachment ?? null
423
448
  const shouldUseAgentMode = agentMode || promptText.toLowerCase().startsWith('search web:')
424
449
  const outgoingImage = outgoingImages.map((img) => img.dataUri).join(' ')
450
+ const outgoingVideo = outgoingVideos.map((vid) => vid.dataUri).join(' ')
425
451
  const outgoingImageCount = outgoingImages.length
426
452
  setSending(true)
427
453
  setStreamingConversationId(conversationId)
428
454
  setSendingMessage(promptText)
429
455
  setSendingImageCount(outgoingImageCount)
456
+ setSendingVideoCount(outgoingVideos.length)
430
457
  setError('')
431
458
  setStreamedReply('')
432
459
  setStreamedResponse(null)
@@ -437,6 +464,7 @@ export default function MintDashboard() {
437
464
  if (options.clearComposer) {
438
465
  setMessage('')
439
466
  setImageAttachments([])
467
+ setVideoAttachments([])
440
468
  setDocumentAttachment(null)
441
469
  }
442
470
 
@@ -446,6 +474,7 @@ export default function MintDashboard() {
446
474
  (chunk) => setStreamedReply((current) => `${current}${chunk}`),
447
475
  outgoingImage,
448
476
  options.audioDataUri ?? null,
477
+ outgoingVideo,
449
478
  options.systemInstruction ?? '',
450
479
  (progress) => {
451
480
  progressSnapshot.push(progress)
@@ -486,6 +515,7 @@ export default function MintDashboard() {
486
515
  setAgentActivitySnapshots((current) => mergeActivitySnapshots(current, enrichedHistory))
487
516
  await refreshChatSessions()
488
517
  await refreshPictures()
518
+ getRuntimeStatus().then(setStatus).catch(() => {})
489
519
  setStreamedReply('')
490
520
  setStreamedResponse(null)
491
521
  } catch (reason) {
@@ -495,6 +525,7 @@ export default function MintDashboard() {
495
525
  setStreamingConversationId(null)
496
526
  setSendingMessage('')
497
527
  setSendingImageCount(0)
528
+ setSendingVideoCount(0)
498
529
  }
499
530
  }
500
531
 
@@ -513,12 +544,91 @@ export default function MintDashboard() {
513
544
  event.preventDefault()
514
545
  const trimmed = message.trim()
515
546
  const currentImages = imageAttachments
547
+ const currentVideos = videoAttachments
516
548
  const currentDocument = documentAttachment
517
- const hasAttachments = currentImages.length > 0 || Boolean(currentDocument)
549
+ const hasAttachments = currentImages.length > 0 || currentVideos.length > 0 || Boolean(currentDocument)
518
550
  if ((!trimmed && !hasAttachments) || sending) return
519
- 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
+ )
520
629
  await sendPrompt(promptText, {
521
630
  imageAttachments: currentImages,
631
+ videoAttachments: currentVideos,
522
632
  documentAttachment: currentDocument,
523
633
  clearComposer: true,
524
634
  })
@@ -548,6 +658,28 @@ export default function MintDashboard() {
548
658
  event.target.value = ''
549
659
  }
550
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
+ }
551
683
 
552
684
  function pasteImage(clipboardData: DataTransfer) {
553
685
  let file: File | null = null
@@ -575,7 +707,28 @@ export default function MintDashboard() {
575
707
  }
576
708
  }
577
709
 
578
- if (!file) return false
710
+ if (!file) {
711
+ // Fallback for Tauri desktop app where WebKitGTK/WebView clipboard paste events do not provide file handles
712
+ const now = Date.now()
713
+ if (now - lastNativePasteTimeRef.current > 100) {
714
+ lastNativePasteTimeRef.current = now
715
+ readClipboardImage().then((dataUri) => {
716
+ if (dataUri) {
717
+ const name = 'Pasted image'
718
+ createTrimmedImagePreview(dataUri)
719
+ .catch(() => dataUri)
720
+ .then((previewDataUri) => {
721
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name }])
722
+ })
723
+ }
724
+ }).catch((err) => {
725
+ console.warn('Failed to read clipboard image via Tauri API:', err)
726
+ })
727
+ }
728
+ return false
729
+ }
730
+
731
+
579
732
 
580
733
  readImage(file)
581
734
  .then((dataUri) => {
@@ -591,6 +744,7 @@ export default function MintDashboard() {
591
744
  }
592
745
 
593
746
 
747
+
594
748
  async function selectDocument(event: ChangeEvent<HTMLInputElement>) {
595
749
  const file = event.target.files?.[0]
596
750
  if (!file) return
@@ -736,6 +890,11 @@ export default function MintDashboard() {
736
890
  else if (provider === 'huggingface') providerName = 'HF'
737
891
  else if (provider === 'local_openai') providerName = 'Local'
738
892
  else if (provider === 'ollama') providerName = 'Ollama'
893
+ else if (provider.startsWith('custom:')) {
894
+ const id = provider.replace(/^custom:/, '')
895
+ const cp = (settingsConfig?.customProviders ?? []).find((p: any) => p.id === id)
896
+ providerName = cp?.displayName || id
897
+ }
739
898
 
740
899
  if (providerName && providerName === provider) {
741
900
  providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
@@ -753,13 +912,21 @@ export default function MintDashboard() {
753
912
  case 'huggingface': return config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct'
754
913
  case 'local_openai': return config.localModelName || 'llama3'
755
914
  case 'ollama': return config.ollamaModel || 'llama3:latest'
756
- default: return ''
915
+ default: {
916
+ if (provider.startsWith('custom:')) {
917
+ const id = provider.replace(/^custom:/, '')
918
+ const cp = (config.customProviders ?? []).find((p: any) => p.id === id)
919
+ return (config.customModelSelections ?? {})[id] ?? cp?.models[0]?.modelId ?? ''
920
+ }
921
+ return ''
922
+ }
757
923
  }
758
924
  }
759
925
 
760
926
  async function changeProvider(provider: string) {
761
927
  try {
762
928
  const config = await window.settingsApi.getSettings()
929
+ if (config.aiProvider === provider) return
763
930
  config.aiProvider = provider
764
931
  await window.settingsApi.saveSettings(config)
765
932
  setSettingsConfig(config)
@@ -768,7 +935,7 @@ export default function MintDashboard() {
768
935
  // Record system event in chat history
769
936
  const activeModel = getActiveModelName(config, provider)
770
937
  const displayName = formatProviderChangeText(provider, activeModel)
771
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
938
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
772
939
  await refreshHistory()
773
940
  } catch (reason) {
774
941
  setError(errorMessage(reason))
@@ -779,6 +946,8 @@ export default function MintDashboard() {
779
946
  try {
780
947
  const config = await window.settingsApi.getSettings()
781
948
  const provider = config.aiProvider
949
+ const currentModel = getActiveModelName(config, provider)
950
+ if (currentModel === modelName) return
782
951
  if (provider === 'gemini') {
783
952
  config.geminiModel = modelName
784
953
  } else if (provider === 'openai') {
@@ -795,6 +964,12 @@ export default function MintDashboard() {
795
964
  config.localModelName = modelName
796
965
  } else if (provider === 'ollama') {
797
966
  config.ollamaModel = modelName
967
+ } else if (provider.startsWith('custom:')) {
968
+ const id = provider.replace(/^custom:/, '')
969
+ config.customModelSelections = {
970
+ ...(config.customModelSelections ?? {}),
971
+ [id]: modelName
972
+ }
798
973
  }
799
974
  await window.settingsApi.saveSettings(config)
800
975
  setSettingsConfig(config)
@@ -802,7 +977,7 @@ export default function MintDashboard() {
802
977
 
803
978
  // Record system event in chat history
804
979
  const displayName = formatProviderChangeText(provider, modelName)
805
- await saveSystemInteraction(conversationId, displayName, 'system', 'provider_change')
980
+ await saveSystemInteraction(conversationId, displayName, '', 'system', 'provider_change')
806
981
  await refreshHistory()
807
982
  } catch (reason) {
808
983
  setError(errorMessage(reason))
@@ -925,7 +1100,7 @@ export default function MintDashboard() {
925
1100
  expressionIndex={expressionIndex}
926
1101
  accessoryIndex={accessoryIndex}
927
1102
  isLocked={isLocked}
928
- isActive={modelVisible && view !== 'pictures' && view !== 'workspace' && view !== 'workflows'}
1103
+ isActive={modelVisible && view !== 'pictures' && view !== 'workspace' && view !== 'workflows' && view !== 'imagine' && view !== 'veo'}
929
1104
  layoutPreset={layoutPreset}
930
1105
  sending={sending}
931
1106
  interactionEnabled={interactionEnabled}
@@ -947,11 +1122,13 @@ export default function MintDashboard() {
947
1122
  streamedReply={streamingConversationId === conversationId ? streamedReply : ''}
948
1123
  streamedResponse={streamingConversationId === conversationId ? streamedResponse : null}
949
1124
  agentProgress={streamingConversationId === conversationId ? agentProgress : []}
1125
+ sendingVideoCount={streamingConversationId === conversationId ? sendingVideoCount : 0}
950
1126
  agentActivitySnapshots={agentActivitySnapshots}
951
1127
  thinkingExpanded={thinkingExpanded}
952
1128
  onThinkingExpandedChange={handleThinkingExpandedChange}
953
1129
  message={message}
954
1130
  imageAttachments={imageAttachments}
1131
+ videoAttachments={videoAttachments}
955
1132
  documentName={documentAttachment?.filename ?? ''}
956
1133
  pendingApproval={streamingConversationId === conversationId ? pendingApproval : null}
957
1134
  smartContext={smartContext}
@@ -962,6 +1139,7 @@ export default function MintDashboard() {
962
1139
  welcomeInteraction={MOCK_WELCOME_INTERACTION}
963
1140
  onSubmit={handleSubmit}
964
1141
  onSelectImage={selectImage}
1142
+ onSelectVideo={selectVideo}
965
1143
  onSelectDocument={selectDocument}
966
1144
  onPasteImage={pasteImage}
967
1145
  onSetMessage={setMessage}
@@ -969,6 +1147,9 @@ export default function MintDashboard() {
969
1147
  onRemoveImage={(idx: number) => {
970
1148
  setImageAttachments((current) => current.filter((_, i) => i !== idx))
971
1149
  }}
1150
+ onRemoveVideo={(idx: number) => {
1151
+ setVideoAttachments((current) => current.filter((_, i) => i !== idx))
1152
+ }}
972
1153
  onRemoveDocument={() => setDocumentAttachment(null)}
973
1154
  onStartWebSearch={startWebSearch}
974
1155
  onCaptureScreen={captureScreen}
@@ -991,6 +1172,13 @@ export default function MintDashboard() {
991
1172
  setMessage(imgPrompt)
992
1173
  }}
993
1174
  />
1175
+ <VeoStudioPanel
1176
+ view={view}
1177
+ onSendToChat={(vidPrompt) => {
1178
+ setView('chat')
1179
+ setMessage(vidPrompt)
1180
+ }}
1181
+ />
994
1182
  <WorkflowBuilderPanel
995
1183
  view={view}
996
1184
  onShowToast={showToast}