@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
@@ -12,9 +12,9 @@ import {
12
12
  selectWorkspaceDirectory,
13
13
  saveInteractionAgentActivity,
14
14
  streamChatMessage,
15
+ cancelChatMessage,
15
16
  submitToolApproval,
16
17
  listen,
17
- readClipboardImage as readTauriClipboardImage,
18
18
  type AgentProgress,
19
19
  type ChatResponse,
20
20
  type ChatSession,
@@ -30,6 +30,13 @@ import type { ModelInteraction } from './ModelPanel'
30
30
  import PicturesLibrary from './PicturesLibrary'
31
31
  import WorkspacePanel from './WorkspacePanel'
32
32
  import WorkflowBuilderPanel from './WorkflowBuilderPanel'
33
+ import {
34
+ errorMessage,
35
+ readImage,
36
+ readDocument,
37
+ createTrimmedImagePreview,
38
+ applyThemeStyles,
39
+ } from '../../shared/utils/ui'
33
40
 
34
41
  const EXPRESSIONS = [
35
42
  "Default",
@@ -88,142 +95,6 @@ const MOCK_WELCOME_INTERACTION = {
88
95
  createdAt: new Date().toISOString(),
89
96
  }
90
97
 
91
- function errorMessage(reason: unknown) {
92
- return reason instanceof Error ? reason.message : String(reason)
93
- }
94
-
95
- function readImage(file: File): Promise<string> {
96
- return new Promise((resolve, reject) => {
97
- const reader = new FileReader()
98
- reader.onload = () => resolve(String(reader.result))
99
- reader.onerror = () => reject(reader.error ?? new Error('Unable to read image'))
100
- reader.readAsDataURL(file)
101
- })
102
- }
103
-
104
- function readDocument(file: File): Promise<string> {
105
- return new Promise((resolve, reject) => {
106
- const reader = new FileReader()
107
- reader.onload = () => resolve(String(reader.result))
108
- reader.onerror = () => reject(reader.error ?? new Error('Unable to read document'))
109
- reader.readAsDataURL(file)
110
- })
111
- }
112
-
113
- async function createTrimmedImagePreview(dataUri: string): Promise<string> {
114
- const image = await new Promise<HTMLImageElement>((resolve, reject) => {
115
- const nextImage = new Image()
116
- nextImage.onload = () => resolve(nextImage)
117
- nextImage.onerror = () => reject(new Error('Unable to prepare image preview'))
118
- nextImage.src = dataUri
119
- })
120
-
121
- const canvas = document.createElement('canvas')
122
- canvas.width = image.naturalWidth || image.width
123
- canvas.height = image.naturalHeight || image.height
124
- const context = canvas.getContext('2d', { willReadFrequently: true })
125
- if (!context || canvas.width === 0 || canvas.height === 0) return dataUri
126
-
127
- context.drawImage(image, 0, 0)
128
- const pixels = context.getImageData(0, 0, canvas.width, canvas.height)
129
- let minX = canvas.width
130
- let minY = canvas.height
131
- let maxX = -1
132
- let maxY = -1
133
-
134
- for (let y = 0; y < canvas.height; y += 1) {
135
- for (let x = 0; x < canvas.width; x += 1) {
136
- const alpha = pixels.data[(y * canvas.width + x) * 4 + 3]
137
- if (alpha > 12) {
138
- minX = Math.min(minX, x)
139
- minY = Math.min(minY, y)
140
- maxX = Math.max(maxX, x)
141
- maxY = Math.max(maxY, y)
142
- }
143
- }
144
- }
145
-
146
- if (maxX < minX || maxY < minY) return dataUri
147
-
148
- const padding = 8
149
- const sx = Math.max(0, minX - padding)
150
- const sy = Math.max(0, minY - padding)
151
- const sw = Math.min(canvas.width - sx, maxX - minX + 1 + padding * 2)
152
- const sh = Math.min(canvas.height - sy, maxY - minY + 1 + padding * 2)
153
-
154
- if (sw >= canvas.width * 0.92 && sh >= canvas.height * 0.92) return dataUri
155
-
156
- const previewCanvas = document.createElement('canvas')
157
- previewCanvas.width = sw
158
- previewCanvas.height = sh
159
- const previewContext = previewCanvas.getContext('2d')
160
- if (!previewContext) return dataUri
161
- previewContext.drawImage(canvas, sx, sy, sw, sh, 0, 0, sw, sh)
162
- return previewCanvas.toDataURL('image/png')
163
- }
164
-
165
- const lightenColor = (hex: string, amount: number) => {
166
- const clean = hex.replace('#', '')
167
- if (clean.length !== 6) return hex
168
- const num = parseInt(clean, 16)
169
- const r = Math.min(255, (num >> 16) + amount)
170
- const g = Math.min(255, ((num >> 8) & 0x00FF) + amount)
171
- const b = Math.min(255, (num & 0x0000FF) + amount)
172
- return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
173
- }
174
-
175
- const hexToRgb = (hex: string) => {
176
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
177
- return result ? {
178
- r: parseInt(result[1], 16),
179
- g: parseInt(result[2], 16),
180
- b: parseInt(result[3], 16),
181
- } : { r: 15, g: 23, b: 42 }
182
- }
183
-
184
- const applyThemeStyles = (cfg: any) => {
185
- const theme = cfg.theme || 'dark'
186
- const accentColor = cfg.accentColor || '#4f83e6'
187
- const systemTextColor = cfg.systemTextColor || '#f8fafc'
188
-
189
- document.documentElement.setAttribute('data-theme', theme)
190
- document.documentElement.style.setProperty('--accent', accentColor)
191
- document.documentElement.style.setProperty('--accent-hover', lightenColor(accentColor, 20))
192
- document.documentElement.style.setProperty('--text-main', systemTextColor)
193
- document.documentElement.style.setProperty('--glass-blur', cfg.glassBlur || 'blur(16px)')
194
- document.body.style.fontFamily = cfg.fontFamily || "'Outfit', sans-serif"
195
- document.documentElement.style.fontSize = cfg.fontSize || '18px'
196
-
197
- if (theme === 'custom') {
198
- if (cfg.customBgStart && cfg.customBgEnd) {
199
- document.documentElement.style.setProperty('--bg-color', cfg.customBgStart)
200
- document.documentElement.style.setProperty('--bg-gradient', `linear-gradient(135deg, ${cfg.customBgStart} 0%, ${cfg.customBgEnd} 100%)`)
201
- }
202
- if (cfg.customPanelBg) {
203
- const rgb = hexToRgb(cfg.customPanelBg)
204
- document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`)
205
- document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`)
206
- document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`)
207
- document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`)
208
- document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`)
209
- document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`)
210
- document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`)
211
- }
212
- return
213
- }
214
-
215
- ;[
216
- '--bg-color',
217
- '--bg-gradient',
218
- '--panel-bg',
219
- '--panel-raised',
220
- '--panel-soft',
221
- '--chrome-bg',
222
- '--surface-bg',
223
- '--surface-strong',
224
- '--input-bg',
225
- ].forEach((name) => document.documentElement.style.removeProperty(name))
226
- }
227
98
 
228
99
  export default function MintDashboard() {
229
100
  const [view, setView] = useState<DashboardView>('chat')
@@ -522,6 +393,20 @@ export default function MintDashboard() {
522
393
  }
523
394
  }
524
395
 
396
+ async function handleCancelMessage() {
397
+ if (!sending || !streamingConversationId) return
398
+ try {
399
+ await cancelChatMessage(streamingConversationId)
400
+ } catch (e) {
401
+ console.error("Failed to cancel message stream:", e)
402
+ } finally {
403
+ setSending(false)
404
+ setStreamingConversationId(null)
405
+ setSendingMessage('')
406
+ setSendingImageCount(0)
407
+ }
408
+ }
409
+
525
410
  async function sendPrompt(
526
411
  promptText: string,
527
412
  options: {
@@ -705,37 +590,6 @@ export default function MintDashboard() {
705
590
  return true
706
591
  }
707
592
 
708
- async function readClipboardImage() {
709
- try {
710
- const dataUri = await readTauriClipboardImage()
711
- if (dataUri) {
712
- const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
713
- setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
714
- return true
715
- }
716
- } catch (err) {
717
- console.warn('Tauri clipboard fallback error:', err)
718
- }
719
-
720
- try {
721
- if (!navigator.clipboard?.read) return false
722
- const items = await navigator.clipboard.read()
723
- for (const item of items) {
724
- const imageType = item.types.find((type) => type.startsWith('image/'))
725
- if (!imageType) continue
726
- const blob = await item.getType(imageType)
727
- const file = new File([blob], 'Pasted image', { type: imageType })
728
- const dataUri = await readImage(file)
729
- const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
730
- setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
731
- return true
732
- }
733
- return false
734
- } catch {
735
- // Some environments expose pasted images only through ClipboardEvent.
736
- return false
737
- }
738
- }
739
593
 
740
594
  async function selectDocument(event: ChangeEvent<HTMLInputElement>) {
741
595
  const file = event.target.files?.[0]
@@ -1110,7 +964,6 @@ export default function MintDashboard() {
1110
964
  onSelectImage={selectImage}
1111
965
  onSelectDocument={selectDocument}
1112
966
  onPasteImage={pasteImage}
1113
- onReadClipboardImage={readClipboardImage}
1114
967
  onSetMessage={setMessage}
1115
968
  onSendVoiceMessage={sendVoiceMessage}
1116
969
  onRemoveImage={(idx: number) => {
@@ -1126,6 +979,7 @@ export default function MintDashboard() {
1126
979
  settingsConfig={settingsConfig}
1127
980
  onSetModel={changeModel}
1128
981
  onApproval={handleApproval}
982
+ onCancelMessage={handleCancelMessage}
1129
983
  />
1130
984
  </main>
1131
985
  <PicturesLibrary view={view} pictures={pictures} onSetView={setView} onRefreshPictures={refreshPictures} />
@@ -1,3 +1,4 @@
1
+ import { useEffect, useMemo, useState } from 'react'
1
2
  import { type PictureEntry, convertFileSrc } from '../tauri'
2
3
  import type { DashboardView } from './DashboardSidebar'
3
4
 
@@ -9,6 +10,16 @@ interface PicturesLibraryProps {
9
10
  }
10
11
 
11
12
  export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPictures }: PicturesLibraryProps) {
13
+ const [visibleCount, setVisibleCount] = useState(24)
14
+ const visiblePictures = useMemo(
15
+ () => pictures.slice(0, visibleCount),
16
+ [pictures, visibleCount],
17
+ )
18
+
19
+ useEffect(() => {
20
+ setVisibleCount(24)
21
+ }, [view, pictures])
22
+
12
23
  return (
13
24
  <section className={`pictures-library ${view === 'pictures' ? 'is-visible' : ''}`} aria-hidden={view !== 'pictures'}>
14
25
  <header className="pictures-header">
@@ -34,14 +45,27 @@ export default function PicturesLibrary({ view, pictures, onSetView, onRefreshPi
34
45
  <span>Images appear here after a message with an attachment is sent successfully.</span>
35
46
  </div>
36
47
  ) : (
37
- <div className="pictures-grid">
38
- {pictures.map((picture, index) => (
39
- <article className="picture-card" key={picture.id}>
40
- <img src={convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || picture.path)} alt={picture.message || picture.filename} loading={index < 6 ? 'eager' : 'lazy'} decoding="async" />
41
- <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
42
- </article>
43
- ))}
44
- </div>
48
+ <>
49
+ <div className="pictures-grid">
50
+ {visiblePictures.map((picture, index) => (
51
+ <article className="picture-card" key={picture.id}>
52
+ <img src={convertFileSrc(picture.thumbnailPath || picture.thumbnailUrl || picture.path)} alt={picture.message || picture.filename} loading={index < 6 ? 'eager' : 'lazy'} decoding="async" />
53
+ <div className="picture-card-meta"><span>{picture.message || picture.filename}</span></div>
54
+ </article>
55
+ ))}
56
+ </div>
57
+ {visibleCount < pictures.length && (
58
+ <div className="pictures-load-more-container">
59
+ <button
60
+ type="button"
61
+ className="pictures-load-more-btn"
62
+ onClick={() => setVisibleCount((prev) => prev + 24)}
63
+ >
64
+ Load More ({pictures.length - visibleCount} remaining)
65
+ </button>
66
+ </div>
67
+ )}
68
+ </>
45
69
  )}
46
70
  </section>
47
71
  )
@@ -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
+ }
@@ -168,28 +168,6 @@ export default function AutomationTab({
168
168
  </div>
169
169
  </section>
170
170
 
171
- <section className="setting-section">
172
- <div className="section-heading">
173
- <div>
174
- <p className="section-kicker">Code mode</p>
175
- <h2 className="section-title">Agent Collaboration</h2>
176
- </div>
177
- </div>
178
- <div className="toggle-row">
179
- <div>
180
- <label>Enable Multi-Agent Review</label>
181
- <p className="hint">Allow a secondary model to review code written by the primary model.</p>
182
- </div>
183
- <label className="settings-toggle-switch">
184
- <input
185
- type="checkbox"
186
- checked={config.enableAgentCollaboration}
187
- onChange={(e) => updateField('enableAgentCollaboration', e.target.checked)}
188
- />
189
- <span className="settings-toggle-slider"></span>
190
- </label>
191
- </div>
192
- </section>
193
171
  </div>
194
172
  )
195
173
  }