@pheem49/mint 1.8.1 → 1.8.2

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 (184) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +0 -17
  4. package/Release_Note.md +92 -58
  5. package/bin/mint +0 -0
  6. package/crates/mint-cli/src/agent.rs +100 -6
  7. package/crates/mint-cli/src/main.rs +166 -52
  8. package/crates/mint-core/src/agent_loop.rs +1 -0
  9. package/crates/mint-core/src/api_server.rs +55 -2
  10. package/crates/mint-core/src/browser.rs +55 -0
  11. package/crates/mint-core/src/channels.rs +1 -0
  12. package/crates/mint-core/src/chat.rs +3 -0
  13. package/crates/mint-core/src/config.rs +61 -0
  14. package/crates/mint-core/src/lib.rs +14 -0
  15. package/crates/mint-core/src/orchestration.rs +180 -11
  16. package/crates/mint-core/src/tts.rs +3 -1
  17. package/flatpak/com.pheem49.mint.desktop +9 -0
  18. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  19. package/flatpak/com.pheem49.mint.yaml +52 -0
  20. package/package.json +1 -1
  21. package/shared-modules/.github/workflows/ci.yml +120 -0
  22. package/shared-modules/.github/workflows/update.yaml +28 -0
  23. package/shared-modules/CODEOWNERS +23 -0
  24. package/shared-modules/README.md +79 -0
  25. package/shared-modules/SDL/README.md +3 -0
  26. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  27. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  28. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  29. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  30. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  31. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  32. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  33. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  34. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  35. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  36. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  37. package/shared-modules/SDL/sdl12-compat.json +22 -0
  38. package/shared-modules/SDL2/SDL2_image.json +27 -0
  39. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  40. package/shared-modules/cld2/CMakeLists.txt +157 -0
  41. package/shared-modules/cld2/cld2.json +24 -0
  42. package/shared-modules/clutter/clutter.json +72 -0
  43. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  44. package/shared-modules/glew/glew.json +36 -0
  45. package/shared-modules/glu/glu-9.json +12 -0
  46. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  47. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  48. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  49. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  50. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  51. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  52. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  53. package/shared-modules/gtk2/gtk2.json +120 -0
  54. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  55. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  56. package/shared-modules/gtk2/readme.md +10 -0
  57. package/shared-modules/intltool/intltool-0.51.json +20 -0
  58. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  59. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  60. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  61. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  62. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  63. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  64. package/shared-modules/lame/lame-3.99.5.json +32 -0
  65. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  66. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  67. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  68. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  69. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  70. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  71. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  72. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  73. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  74. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  75. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  76. package/shared-modules/libcanberra/libcanberra.json +44 -0
  77. package/shared-modules/libmad/libmad.json +19 -0
  78. package/shared-modules/libsecret/libsecret.json +23 -0
  79. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  80. package/shared-modules/libusb/libusb.json +27 -0
  81. package/shared-modules/linux-audio/dssi.json +31 -0
  82. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  83. package/shared-modules/linux-audio/fftw3f.json +42 -0
  84. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  85. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  86. package/shared-modules/linux-audio/ladspa.json +26 -0
  87. package/shared-modules/linux-audio/lash.json +35 -0
  88. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  89. package/shared-modules/linux-audio/liblo-static.json +22 -0
  90. package/shared-modules/linux-audio/liblo.json +17 -0
  91. package/shared-modules/linux-audio/lilv-static.json +94 -0
  92. package/shared-modules/linux-audio/lilv.json +82 -0
  93. package/shared-modules/linux-audio/lrdf.json +57 -0
  94. package/shared-modules/linux-audio/lv2.json +21 -0
  95. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  96. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  97. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  98. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  99. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  100. package/shared-modules/linux-audio/stk.json +28 -0
  101. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  102. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  103. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  104. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  105. package/shared-modules/lua5.3/lua.pc.in +8 -0
  106. package/shared-modules/lua5.3/luaconf.patch +47 -0
  107. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  108. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  109. package/shared-modules/lua5.4/lua.pc.in +8 -0
  110. package/shared-modules/lua5.4/luaconf.patch +34 -0
  111. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  112. package/shared-modules/luajit/luajit.json +30 -0
  113. package/shared-modules/lzo/lzo.json +27 -0
  114. package/shared-modules/mac/mac.json +18 -0
  115. package/shared-modules/physfs/physfs.json +26 -0
  116. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  117. package/shared-modules/pygame/cython-fixes.patch +34 -0
  118. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  119. package/shared-modules/pygame/glib.patch +25 -0
  120. package/shared-modules/pygame/pygame1-python2.json +157 -0
  121. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  122. package/shared-modules/pygtk/pygtk.json +117 -0
  123. package/shared-modules/pygtk/pygtk.patch +39 -0
  124. package/shared-modules/python2.7/python-2.7.json +48 -0
  125. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  126. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  127. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  128. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  129. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  130. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  131. package/shared-modules/update-checker.sh +50 -0
  132. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  133. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  134. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  135. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  136. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  137. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  138. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  139. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  140. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  141. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  142. package/shared-modules/vorbisgain/README.md +23 -0
  143. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  144. package/src/renderer/shared/agentProgress.ts +145 -0
  145. package/src/renderer/shared/calculator.ts +52 -0
  146. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  147. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  148. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  149. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  150. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  151. package/src/renderer/shared/constants/models.ts +91 -0
  152. package/src/renderer/shared/platform.ts +82 -0
  153. package/src/renderer/shared/types.ts +140 -0
  154. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  155. package/src/renderer/shared/utils/approval.ts +1 -0
  156. package/src/renderer/shared/utils/approval.tsx +64 -0
  157. package/src/renderer/shared/utils/markdown.tsx +347 -0
  158. package/src/renderer/shared/utils/providers.ts +36 -0
  159. package/src/renderer/shared/utils/speech.ts +216 -0
  160. package/src/renderer/shared/utils/ui.ts +147 -0
  161. package/src/renderer/src/agentProgress.ts +11 -39
  162. package/src/renderer/src/calculator.ts +3 -46
  163. package/src/renderer/src/components/ChatPanel.tsx +194 -1303
  164. package/src/renderer/src/components/MintDashboard.tsx +23 -169
  165. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  166. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  167. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  168. package/src/renderer/src/components/SettingsWindow.tsx +53 -3
  169. package/src/renderer/src/css/styles.css +84 -0
  170. package/src/renderer/src/tauri.ts +128 -143
  171. package/src/renderer/src-web/agentProgress.ts +11 -39
  172. package/src/renderer/src-web/calculator.ts +3 -46
  173. package/src/renderer/src-web/components/ChatPanel.tsx +194 -1301
  174. package/src/renderer/src-web/components/MintDashboard.tsx +23 -169
  175. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  176. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  177. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  178. package/src/renderer/src-web/components/SettingsWindow.tsx +53 -3
  179. package/src/renderer/src-web/css/styles.css +84 -0
  180. package/src/renderer/src-web/tauri.ts +180 -102
  181. package/src-tauri/src/lib.rs +137 -33
  182. package/src-tauri/src/webhooks.rs +1 -0
  183. package/src-tauri/src/workflows.rs +1 -0
  184. package/src-tauri/tauri.conf.json +1 -1
@@ -11,9 +11,9 @@ import {
11
11
  listSavedPictures,
12
12
  saveInteractionAgentActivity,
13
13
  streamChatMessage,
14
+ cancelChatMessage,
14
15
  submitToolApproval,
15
16
  listen,
16
- readClipboardImage as readTauriClipboardImage,
17
17
  type AgentProgress,
18
18
  type ChatResponse,
19
19
  type ChatSession,
@@ -25,6 +25,13 @@ import ChatPanel from './ChatPanel'
25
25
  import DashboardSidebar, { type DashboardView } from './DashboardSidebar'
26
26
  import PicturesLibrary from './PicturesLibrary'
27
27
  import ImageStudioPanel from './ImageStudioPanel'
28
+ import {
29
+ errorMessage,
30
+ readImage,
31
+ readDocument,
32
+ createTrimmedImagePreview,
33
+ applyThemeStyles,
34
+ } from '../../shared/utils/ui'
28
35
 
29
36
  const DEFAULT_CONFIG = {
30
37
  theme: 'dark',
@@ -66,142 +73,6 @@ const MOCK_WELCOME_INTERACTION = {
66
73
  createdAt: new Date().toISOString(),
67
74
  }
68
75
 
69
- function errorMessage(reason: unknown) {
70
- return reason instanceof Error ? reason.message : String(reason)
71
- }
72
-
73
- function readImage(file: File): Promise<string> {
74
- return new Promise((resolve, reject) => {
75
- const reader = new FileReader()
76
- reader.onload = () => resolve(String(reader.result))
77
- reader.onerror = () => reject(reader.error ?? new Error('Unable to read image'))
78
- reader.readAsDataURL(file)
79
- })
80
- }
81
-
82
- function readDocument(file: File): Promise<string> {
83
- return new Promise((resolve, reject) => {
84
- const reader = new FileReader()
85
- reader.onload = () => resolve(String(reader.result))
86
- reader.onerror = () => reject(reader.error ?? new Error('Unable to read document'))
87
- reader.readAsDataURL(file)
88
- })
89
- }
90
-
91
- async function createTrimmedImagePreview(dataUri: string): Promise<string> {
92
- const image = await new Promise<HTMLImageElement>((resolve, reject) => {
93
- const nextImage = new Image()
94
- nextImage.onload = () => resolve(nextImage)
95
- nextImage.onerror = () => reject(new Error('Unable to prepare image preview'))
96
- nextImage.src = dataUri
97
- })
98
-
99
- const canvas = document.createElement('canvas')
100
- canvas.width = image.naturalWidth || image.width
101
- canvas.height = image.naturalHeight || image.height
102
- const context = canvas.getContext('2d', { willReadFrequently: true })
103
- if (!context || canvas.width === 0 || canvas.height === 0) return dataUri
104
-
105
- context.drawImage(image, 0, 0)
106
- const pixels = context.getImageData(0, 0, canvas.width, canvas.height)
107
- let minX = canvas.width
108
- let minY = canvas.height
109
- let maxX = -1
110
- let maxY = -1
111
-
112
- for (let y = 0; y < canvas.height; y += 1) {
113
- for (let x = 0; x < canvas.width; x += 1) {
114
- const alpha = pixels.data[(y * canvas.width + x) * 4 + 3]
115
- if (alpha > 12) {
116
- minX = Math.min(minX, x)
117
- minY = Math.min(minY, y)
118
- maxX = Math.max(maxX, x)
119
- maxY = Math.max(maxY, y)
120
- }
121
- }
122
- }
123
-
124
- if (maxX < minX || maxY < minY) return dataUri
125
-
126
- const padding = 8
127
- const sx = Math.max(0, minX - padding)
128
- const sy = Math.max(0, minY - padding)
129
- const sw = Math.min(canvas.width - sx, maxX - minX + 1 + padding * 2)
130
- const sh = Math.min(canvas.height - sy, maxY - minY + 1 + padding * 2)
131
-
132
- if (sw >= canvas.width * 0.92 && sh >= canvas.height * 0.92) return dataUri
133
-
134
- const previewCanvas = document.createElement('canvas')
135
- previewCanvas.width = sw
136
- previewCanvas.height = sh
137
- const previewContext = previewCanvas.getContext('2d')
138
- if (!previewContext) return dataUri
139
- previewContext.drawImage(canvas, sx, sy, sw, sh, 0, 0, sw, sh)
140
- return previewCanvas.toDataURL('image/png')
141
- }
142
-
143
- const lightenColor = (hex: string, amount: number) => {
144
- const clean = hex.replace('#', '')
145
- if (clean.length !== 6) return hex
146
- const num = parseInt(clean, 16)
147
- const r = Math.min(255, (num >> 16) + amount)
148
- const g = Math.min(255, ((num >> 8) & 0x00FF) + amount)
149
- const b = Math.min(255, (num & 0x0000FF) + amount)
150
- return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
151
- }
152
-
153
- const hexToRgb = (hex: string) => {
154
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
155
- return result ? {
156
- r: parseInt(result[1], 16),
157
- g: parseInt(result[2], 16),
158
- b: parseInt(result[3], 16),
159
- } : { r: 15, g: 23, b: 42 }
160
- }
161
-
162
- const applyThemeStyles = (cfg: any) => {
163
- const theme = cfg.theme || 'dark'
164
- const accentColor = cfg.accentColor || '#4f83e6'
165
- const systemTextColor = cfg.systemTextColor || '#f8fafc'
166
-
167
- document.documentElement.setAttribute('data-theme', theme)
168
- document.documentElement.style.setProperty('--accent', accentColor)
169
- document.documentElement.style.setProperty('--accent-hover', lightenColor(accentColor, 20))
170
- document.documentElement.style.setProperty('--text-main', systemTextColor)
171
- document.documentElement.style.setProperty('--glass-blur', cfg.glassBlur || 'blur(16px)')
172
- document.body.style.fontFamily = cfg.fontFamily || "'Outfit', sans-serif"
173
- document.documentElement.style.fontSize = cfg.fontSize || '18px'
174
-
175
- if (theme === 'custom') {
176
- if (cfg.customBgStart && cfg.customBgEnd) {
177
- document.documentElement.style.setProperty('--bg-color', cfg.customBgStart)
178
- document.documentElement.style.setProperty('--bg-gradient', `linear-gradient(135deg, ${cfg.customBgStart} 0%, ${cfg.customBgEnd} 100%)`)
179
- }
180
- if (cfg.customPanelBg) {
181
- const rgb = hexToRgb(cfg.customPanelBg)
182
- document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`)
183
- document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`)
184
- document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`)
185
- document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`)
186
- document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`)
187
- document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`)
188
- document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`)
189
- }
190
- return
191
- }
192
-
193
- ;[
194
- '--bg-color',
195
- '--bg-gradient',
196
- '--panel-bg',
197
- '--panel-raised',
198
- '--panel-soft',
199
- '--chrome-bg',
200
- '--surface-bg',
201
- '--surface-strong',
202
- '--input-bg',
203
- ].forEach((name) => document.documentElement.style.removeProperty(name))
204
- }
205
76
 
206
77
  export default function MintDashboard() {
207
78
  const [view, setView] = useState<DashboardView>('chat')
@@ -415,6 +286,20 @@ export default function MintDashboard() {
415
286
  }
416
287
  }
417
288
 
289
+ async function handleCancelMessage() {
290
+ if (!sending || !streamingConversationId) return
291
+ try {
292
+ await cancelChatMessage(streamingConversationId)
293
+ } catch (e) {
294
+ console.error("Failed to cancel message stream:", e)
295
+ } finally {
296
+ setSending(false)
297
+ setStreamingConversationId(null)
298
+ setSendingMessage('')
299
+ setSendingImageCount(0)
300
+ }
301
+ }
302
+
418
303
  async function sendPrompt(
419
304
  promptText: string,
420
305
  options: {
@@ -587,37 +472,6 @@ export default function MintDashboard() {
587
472
  return true
588
473
  }
589
474
 
590
- async function readClipboardImage() {
591
- try {
592
- const dataUri = await readTauriClipboardImage()
593
- if (dataUri) {
594
- const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
595
- setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
596
- return true
597
- }
598
- } catch (err) {
599
- console.warn('Tauri clipboard fallback error:', err)
600
- }
601
-
602
- try {
603
- if (!navigator.clipboard?.read) return false
604
- const items = await navigator.clipboard.read()
605
- for (const item of items) {
606
- const imageType = item.types.find((type) => type.startsWith('image/'))
607
- if (!imageType) continue
608
- const blob = await item.getType(imageType)
609
- const file = new File([blob], 'Pasted image', { type: imageType })
610
- const dataUri = await readImage(file)
611
- const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
612
- setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
613
- return true
614
- }
615
- return false
616
- } catch {
617
- // Some environments expose pasted images only through ClipboardEvent.
618
- return false
619
- }
620
- }
621
475
 
622
476
  async function selectDocument(event: ChangeEvent<HTMLInputElement>) {
623
477
  const file = event.target.files?.[0]
@@ -886,7 +740,6 @@ export default function MintDashboard() {
886
740
  onSelectImage={selectImage}
887
741
  onSelectDocument={selectDocument}
888
742
  onPasteImage={pasteImage}
889
- onReadClipboardImage={readClipboardImage}
890
743
  onSetMessage={setMessage}
891
744
  onSendVoiceMessage={sendVoiceMessage}
892
745
  onRemoveImage={(idx: number) => {
@@ -901,6 +754,7 @@ export default function MintDashboard() {
901
754
  settingsConfig={settingsConfig}
902
755
  onSetModel={changeModel}
903
756
  onApproval={handleApproval}
757
+ onCancelMessage={handleCancelMessage}
904
758
  onToggleMobileSidebar={() => setMobileSidebarOpen(!mobileSidebarOpen)}
905
759
  />
906
760
  </main>
@@ -2,10 +2,6 @@ import { useEffect, useMemo, useState } from 'react'
2
2
  import { type PictureEntry, convertFileSrc } from '../tauri'
3
3
  import type { DashboardView } from './DashboardSidebar'
4
4
 
5
- const INITIAL_VISIBLE_PICTURES = 18
6
- const PICTURE_RENDER_BATCH_SIZE = 18
7
- const PICTURE_RENDER_BATCH_DELAY_MS = 80
8
-
9
5
  interface PicturesLibraryProps {
10
6
  view: DashboardView
11
7
  pictures: PictureEntry[]
@@ -14,31 +10,16 @@ interface PicturesLibraryProps {
14
10
  }
15
11
 
16
12
  export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPictures }: PicturesLibraryProps) {
17
- const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_PICTURES)
13
+ const [visibleCount, setVisibleCount] = useState(24)
18
14
  const visiblePictures = useMemo(
19
15
  () => pictures.slice(0, visibleCount),
20
16
  [pictures, visibleCount],
21
17
  )
22
18
 
23
19
  useEffect(() => {
24
- if (view !== 'pictures') {
25
- setVisibleCount(INITIAL_VISIBLE_PICTURES)
26
- return
27
- }
28
-
29
- setVisibleCount(INITIAL_VISIBLE_PICTURES)
20
+ setVisibleCount(24)
30
21
  }, [view, pictures])
31
22
 
32
- useEffect(() => {
33
- if (view !== 'pictures' || visibleCount >= pictures.length) return
34
-
35
- const timer = window.setTimeout(() => {
36
- setVisibleCount((current) => Math.min(current + PICTURE_RENDER_BATCH_SIZE, pictures.length))
37
- }, PICTURE_RENDER_BATCH_DELAY_MS)
38
-
39
- return () => window.clearTimeout(timer)
40
- }, [view, pictures.length, visibleCount])
41
-
42
23
  return (
43
24
  <section className={`pictures-library ${view === 'pictures' ? 'is-visible' : ''}`} aria-hidden={view !== 'pictures'}>
44
25
  <header className="pictures-header">
@@ -61,14 +42,27 @@ export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPi
61
42
  <span>Images appear here after a message with an attachment is sent successfully.</span>
62
43
  </div>
63
44
  ) : (
64
- <div className="pictures-grid">
65
- {visiblePictures.map((picture, index) => (
66
- <article className="picture-card" key={picture.id}>
67
- <img src={convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || picture.path)} alt={picture.message || picture.filename} loading={index < 6 ? 'eager' : 'lazy'} decoding="async" />
68
- <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
69
- </article>
70
- ))}
71
- </div>
45
+ <>
46
+ <div className="pictures-grid">
47
+ {visiblePictures.map((picture, index) => (
48
+ <article className="picture-card" key={picture.id}>
49
+ <img src={convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || picture.path)} alt={picture.message || picture.filename} loading={index < 6 ? 'eager' : 'lazy'} decoding="async" />
50
+ <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
51
+ </article>
52
+ ))}
53
+ </div>
54
+ {visibleCount < pictures.length && (
55
+ <div className="pictures-load-more-container">
56
+ <button
57
+ type="button"
58
+ className="pictures-load-more-btn"
59
+ onClick={() => setVisibleCount((prev) => prev + 24)}
60
+ >
61
+ Load More ({pictures.length - visibleCount} remaining)
62
+ </button>
63
+ </div>
64
+ )}
65
+ </>
72
66
  )}
73
67
  </section>
74
68
  )
@@ -0,0 +1,362 @@
1
+ import React, { useState } from 'react'
2
+ import {
3
+ DEFAULT_CONFIG,
4
+ GEMINI_MODELS,
5
+ OPENAI_MODELS,
6
+ OPENROUTER_MODELS,
7
+ DEEPSEEK_MODELS,
8
+ ANTHROPIC_MODELS,
9
+ HF_MODELS,
10
+ LOCAL_MODELS
11
+ } from '../SettingsWindow'
12
+
13
+ interface Agent {
14
+ id: string
15
+ name: string
16
+ provider: string
17
+ model: string
18
+ apiKey?: string
19
+ systemInstruction: string
20
+ enabled: boolean
21
+ }
22
+
23
+ interface AgentsTabProps {
24
+ config: typeof DEFAULT_CONFIG & { agents?: Agent[] }
25
+ updateField: (field: any, value: any) => void
26
+ dynamicOllamaModels?: string[]
27
+ }
28
+
29
+ export default function AgentsTab({ config, updateField, dynamicOllamaModels = [] }: AgentsTabProps) {
30
+ const agents = config.agents || []
31
+ const [editingAgent, setEditingAgent] = useState<Agent | null>(null)
32
+ const [name, setName] = useState('')
33
+ const [provider, setProvider] = useState('gemini')
34
+ const [model, setModel] = useState('gemini-2.5-flash')
35
+ const [apiKey, setApiKey] = useState('')
36
+ const [systemInstruction, setSystemInstruction] = useState('')
37
+ const [isEditing, setIsEditing] = useState(false)
38
+
39
+ const handleProviderChange = (newProvider: string) => {
40
+ setProvider(newProvider)
41
+ let defaultModel = ''
42
+ if (newProvider === 'gemini') defaultModel = config.geminiModel || GEMINI_MODELS[0]
43
+ else if (newProvider === 'openai') defaultModel = config.openaiModel || OPENAI_MODELS[0]
44
+ else if (newProvider === 'anthropic') defaultModel = config.anthropicModel || ANTHROPIC_MODELS[0]
45
+ else if (newProvider === 'openrouter') defaultModel = config.openrouterModel || OPENROUTER_MODELS[0]
46
+ else if (newProvider === 'deepseek') defaultModel = config.deepseekModel || DEEPSEEK_MODELS[0]
47
+ else if (newProvider === 'huggingface') defaultModel = config.hfModel || HF_MODELS[0]
48
+ else if (newProvider === 'local_openai') defaultModel = config.localModelName || LOCAL_MODELS[0]
49
+ else if (newProvider === 'ollama') defaultModel = config.ollamaModel || dynamicOllamaModels[0] || 'llama3:latest'
50
+ setModel(defaultModel)
51
+ }
52
+
53
+ const handleEdit = (agent: Agent) => {
54
+ setEditingAgent(agent)
55
+ setName(agent.name)
56
+ setProvider(agent.provider)
57
+ setModel(agent.model)
58
+ setApiKey(agent.apiKey || '')
59
+ setSystemInstruction(agent.systemInstruction)
60
+ setIsEditing(true)
61
+ }
62
+
63
+ const handleSaveAgent = () => {
64
+ if (!name.trim()) {
65
+ alert('Please enter an agent name.')
66
+ return
67
+ }
68
+
69
+ let updatedAgents: Agent[] = []
70
+ if (editingAgent) {
71
+ // Update existing agent
72
+ updatedAgents = agents.map(a =>
73
+ a.id === editingAgent.id
74
+ ? { ...a, name, provider, model, apiKey: apiKey || undefined, systemInstruction }
75
+ : a
76
+ )
77
+ } else {
78
+ // Add new agent
79
+ const newAgent: Agent = {
80
+ id: `agent-${Date.now()}`,
81
+ name,
82
+ provider,
83
+ model,
84
+ apiKey: apiKey || undefined,
85
+ systemInstruction,
86
+ enabled: true
87
+ }
88
+ updatedAgents = [...agents, newAgent]
89
+ }
90
+
91
+ updateField('agents', updatedAgents)
92
+ closeForm()
93
+ }
94
+
95
+ const handleDeleteAgent = (id: string) => {
96
+ if (confirm('Are you sure you want to delete this agent?')) {
97
+ const updatedAgents = agents.filter(a => a.id !== id)
98
+ updateField('agents', updatedAgents)
99
+ }
100
+ }
101
+
102
+ const handleToggleAgent = (id: string, enabled: boolean) => {
103
+ const updatedAgents = agents.map(a =>
104
+ a.id === id ? { ...a, enabled } : a
105
+ )
106
+ updateField('agents', updatedAgents)
107
+ }
108
+
109
+ const handleDisableAll = () => {
110
+ const updatedAgents = agents.map(a => ({ ...a, enabled: false }))
111
+ updateField('agents', updatedAgents)
112
+ }
113
+
114
+ const closeForm = () => {
115
+ setEditingAgent(null)
116
+ setName('')
117
+ setProvider('gemini')
118
+ setModel('gemini-2.5-flash')
119
+ setApiKey('')
120
+ setSystemInstruction('')
121
+ setIsEditing(false)
122
+ }
123
+
124
+ const handleAddClick = () => {
125
+ setEditingAgent(null)
126
+ setName('')
127
+ setProvider('gemini')
128
+ setModel('gemini-2.5-flash')
129
+ setApiKey('')
130
+ setSystemInstruction('')
131
+ setIsEditing(true)
132
+ }
133
+
134
+ // Filter providers that have configured API keys, plus local ones
135
+ const availableProviders = [
136
+ { value: 'gemini', label: 'Google Gemini', hasKey: !!config.apiKey },
137
+ { value: 'openai', label: 'OpenAI', hasKey: !!config.openaiApiKey },
138
+ { value: 'anthropic', label: 'Anthropic Claude', hasKey: !!config.anthropicApiKey },
139
+ { value: 'openrouter', label: 'OpenRouter', hasKey: !!config.openrouterApiKey },
140
+ { value: 'deepseek', label: 'DeepSeek', hasKey: !!config.deepseekApiKey },
141
+ { value: 'ollama', label: 'Ollama (Local)', hasKey: true },
142
+ { value: 'huggingface', label: 'Hugging Face', hasKey: !!config.hfApiKey },
143
+ { value: 'local_openai', label: 'Local (LM Studio / OpenAI Compatible)', hasKey: true },
144
+ ].filter(p => p.hasKey);
145
+
146
+ // Find out what models list corresponds to the selected provider
147
+ let modelList: string[] = []
148
+ let defaultGeneralModel = ''
149
+ if (provider === 'gemini') {
150
+ modelList = GEMINI_MODELS
151
+ defaultGeneralModel = config.geminiModel
152
+ } else if (provider === 'openai') {
153
+ modelList = OPENAI_MODELS
154
+ defaultGeneralModel = config.openaiModel
155
+ } else if (provider === 'openrouter') {
156
+ modelList = OPENROUTER_MODELS
157
+ defaultGeneralModel = config.openrouterModel
158
+ } else if (provider === 'deepseek') {
159
+ modelList = DEEPSEEK_MODELS
160
+ defaultGeneralModel = config.deepseekModel
161
+ } else if (provider === 'anthropic') {
162
+ modelList = ANTHROPIC_MODELS
163
+ defaultGeneralModel = config.anthropicModel
164
+ } else if (provider === 'huggingface') {
165
+ modelList = HF_MODELS
166
+ defaultGeneralModel = config.hfModel
167
+ } else if (provider === 'local_openai') {
168
+ modelList = LOCAL_MODELS
169
+ defaultGeneralModel = config.localModelName
170
+ } else if (provider === 'ollama') {
171
+ modelList = dynamicOllamaModels
172
+ defaultGeneralModel = config.ollamaModel
173
+ }
174
+
175
+ // Sort modelList so defaultGeneralModel is first (at the top)
176
+ const sortedModelList = defaultGeneralModel
177
+ ? [defaultGeneralModel, ...modelList.filter(m => m !== defaultGeneralModel)]
178
+ : modelList;
179
+
180
+ const isCustomModel = !sortedModelList.includes(model)
181
+
182
+ return (
183
+ <div className="tab-pane active animate-fade-in" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
184
+ <section className="setting-section">
185
+ <div className="section-heading" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
186
+ <div>
187
+ <p className="section-kicker">Multi-Agent System</p>
188
+ <h2 className="section-title">Agent Collaboration Settings</h2>
189
+ <p className="section-description">
190
+ Configure multiple distinct AI agents with specific roles. You can adjust the models, system instructions, and credentials for each agent.
191
+ </p>
192
+ </div>
193
+ {!isEditing && (
194
+ <div style={{ display: 'flex', gap: '10px', height: 'fit-content', flexShrink: 0 }}>
195
+ <button className="btn btn-secondary" onClick={handleDisableAll} style={{ whiteSpace: 'nowrap' }}>
196
+ Disable All
197
+ </button>
198
+ <button className="btn btn-primary" onClick={handleAddClick} style={{ whiteSpace: 'nowrap' }}>
199
+ + Add Custom Agent
200
+ </button>
201
+ </div>
202
+ )}
203
+ </div>
204
+
205
+ <div className="toggle-row" style={{ margin: '16px 0 24px 0' }}>
206
+ <div>
207
+ <label>Enable Multi-Agent Collaboration</label>
208
+ <p className="hint">Allow multiple specialized agents to collaborate sequentially (Planner → Coder → Reviewer).</p>
209
+ </div>
210
+ <label className="settings-toggle-switch">
211
+ <input
212
+ type="checkbox"
213
+ checked={config.enableAgentCollaboration}
214
+ onChange={(e) => updateField('enableAgentCollaboration', e.target.checked)}
215
+ />
216
+ <span className="settings-toggle-slider"></span>
217
+ </label>
218
+ </div>
219
+
220
+ {isEditing ? (
221
+ <div className="form-grid single" style={{ background: 'var(--panel-soft)', padding: '20px', borderRadius: '8px', border: '1px solid var(--panel-raised)', marginTop: '10px' }}>
222
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '18px', color: 'var(--accent)' }}>
223
+ {editingAgent ? `Edit Agent: ${editingAgent.name}` : 'Create New Agent'}
224
+ </h3>
225
+
226
+ <div className="setting-row">
227
+ <label>Agent Name</label>
228
+ <input
229
+ type="text"
230
+ value={name}
231
+ onChange={(e) => setName(e.target.value)}
232
+ placeholder="e.g. Code Reviewer"
233
+ />
234
+ </div>
235
+
236
+ <div className="setting-row">
237
+ <label>AI Provider</label>
238
+ <select value={provider} onChange={(e) => handleProviderChange(e.target.value)}>
239
+ {availableProviders.map(p => (
240
+ <option key={p.value} value={p.value}>{p.label}</option>
241
+ ))}
242
+ </select>
243
+ </div>
244
+
245
+ <div className="setting-row">
246
+ <label>Model Name</label>
247
+ <select
248
+ value={isCustomModel ? 'custom' : model}
249
+ onChange={(e) => {
250
+ const val = e.target.value
251
+ if (val === 'custom') {
252
+ setModel('')
253
+ } else {
254
+ setModel(val)
255
+ }
256
+ }}
257
+ >
258
+ {sortedModelList.map(m => (
259
+ <option key={m} value={m}>{m}</option>
260
+ ))}
261
+ <option value="custom">Custom...</option>
262
+ </select>
263
+ </div>
264
+
265
+ {isCustomModel && (
266
+ <div className="setting-row">
267
+ <label>Custom Model Name</label>
268
+ <input
269
+ type="text"
270
+ value={model}
271
+ onChange={(e) => setModel(e.target.value)}
272
+ placeholder="Enter custom model name..."
273
+ />
274
+ </div>
275
+ )}
276
+
277
+ <div className="setting-row">
278
+ <label>Custom API Key (Optional)</label>
279
+ <input
280
+ type="password"
281
+ value={apiKey}
282
+ onChange={(e) => setApiKey(e.target.value)}
283
+ placeholder="Leave blank to use global provider API key"
284
+ />
285
+ </div>
286
+
287
+ <div className="setting-row">
288
+ <label>System Instructions / Role</label>
289
+ <textarea
290
+ value={systemInstruction}
291
+ onChange={(e) => setSystemInstruction(e.target.value)}
292
+ placeholder="Describe this agent's goal, instructions, and personality..."
293
+ style={{ minHeight: '120px', resize: 'vertical' }}
294
+ />
295
+ </div>
296
+
297
+ <div style={{ display: 'flex', gap: '10px', marginTop: '16px', justifyContent: 'flex-end' }}>
298
+ <button className="btn btn-secondary" onClick={closeForm}>Cancel</button>
299
+ <button className="btn btn-primary" onClick={handleSaveAgent}>Save Agent</button>
300
+ </div>
301
+ </div>
302
+ ) : (
303
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginTop: '16px' }}>
304
+ {agents.map((agent) => (
305
+ <div
306
+ key={agent.id}
307
+ style={{
308
+ display: 'flex',
309
+ flexDirection: 'column',
310
+ padding: '16px',
311
+ background: 'var(--panel-bg)',
312
+ border: '1px solid var(--panel-raised)',
313
+ borderRadius: '8px',
314
+ gap: '8px'
315
+ }}
316
+ >
317
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
318
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
319
+ <strong style={{ fontSize: '18px' }}>{agent.name}</strong>
320
+ <span
321
+ style={{
322
+ fontSize: '11px',
323
+ padding: '2px 6px',
324
+ borderRadius: '4px',
325
+ background: 'var(--panel-raised)',
326
+ color: 'var(--accent)'
327
+ }}
328
+ >
329
+ {agent.provider} ({agent.model})
330
+ </span>
331
+ </div>
332
+ <div style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
333
+ <label className="settings-toggle-switch">
334
+ <input
335
+ type="checkbox"
336
+ checked={agent.enabled}
337
+ onChange={(e) => handleToggleAgent(agent.id, e.target.checked)}
338
+ />
339
+ <span className="settings-toggle-slider"></span>
340
+ </label>
341
+ <button className="btn btn-secondary" style={{ padding: '4px 8px', fontSize: '12px' }} onClick={() => handleEdit(agent)}>Edit</button>
342
+ {!['planner', 'coder', 'reviewer'].includes(agent.id) && (
343
+ <button className="btn btn-danger" style={{ padding: '4px 8px', fontSize: '12px' }} onClick={() => handleDeleteAgent(agent.id)}>Delete</button>
344
+ )}
345
+ </div>
346
+ </div>
347
+ <p style={{ margin: '4px 0 0 0', fontSize: '14px', color: 'var(--text-soft)', fontStyle: 'italic' }}>
348
+ "{agent.systemInstruction}"
349
+ </p>
350
+ </div>
351
+ ))}
352
+ {agents.length === 0 && (
353
+ <div style={{ textAlign: 'center', padding: '32px', color: 'var(--text-soft)', fontStyle: 'italic' }}>
354
+ No agents configured. Click "+ Add Custom Agent" to get started.
355
+ </div>
356
+ )}
357
+ </div>
358
+ )}
359
+ </section>
360
+ </div>
361
+ )
362
+ }