@pheem49/mint 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +285 -140
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +287 -43
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/interactive.rs +2888 -0
  10. package/crates/mint-cli/src/main.rs +336 -2854
  11. package/crates/mint-cli/src/markdown.rs +294 -0
  12. package/crates/mint-cli/src/onboard.rs +44 -1
  13. package/crates/mint-cli/src/updater.rs +48 -6
  14. package/crates/mint-core/Cargo.toml +1 -0
  15. package/crates/mint-core/src/agent_loop.rs +1 -0
  16. package/crates/mint-core/src/api_server.rs +376 -13
  17. package/crates/mint-core/src/browser.rs +629 -101
  18. package/crates/mint-core/src/channels.rs +105 -6
  19. package/crates/mint-core/src/chat.rs +52 -4
  20. package/crates/mint-core/src/code_tools.rs +29 -11
  21. package/crates/mint-core/src/config.rs +118 -0
  22. package/crates/mint-core/src/image_gen.rs +294 -33
  23. package/crates/mint-core/src/lib.rs +5 -2
  24. package/crates/mint-core/src/mcp.rs +119 -6
  25. package/crates/mint-core/src/orchestration.rs +133 -11
  26. package/crates/mint-core/src/pictures.rs +64 -1
  27. package/crates/mint-core/src/skills.rs +82 -6
  28. package/crates/mint-core/src/video_gen.rs +235 -0
  29. package/package.json +8 -2
  30. package/shared-modules/luajit/luajit.json +2 -2
  31. package/skills-lock.json +41 -0
  32. package/src/renderer/index-web.html +1 -1
  33. package/src/renderer/index.html +1 -1
  34. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  35. package/src/renderer/shared/platform.ts +3 -0
  36. package/src/renderer/shared/types.ts +4 -0
  37. package/src/renderer/shared/utils/approval.tsx +86 -7
  38. package/src/renderer/shared/utils/markdown.tsx +217 -88
  39. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  40. package/src/renderer/src/components/ChatPanel.tsx +461 -64
  41. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  42. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  43. package/src/renderer/src/components/MintDashboard.tsx +150 -5
  44. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  45. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  46. package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
  47. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  48. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  49. package/src/renderer/src/components/SettingsWindow.tsx +38 -2
  50. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  51. package/src/renderer/src/css/image-studio.css +106 -0
  52. package/src/renderer/src/css/settings.css +158 -0
  53. package/src/renderer/src/css/styles.css +352 -1
  54. package/src/renderer/src/css/veo-studio.css +626 -0
  55. package/src/renderer/src/index.css +1 -0
  56. package/src/renderer/src/tauri.ts +144 -17
  57. package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
  58. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  59. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  60. package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
  61. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  62. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  63. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
  64. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  65. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  66. package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
  67. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  68. package/src/renderer/src-web/css/image-studio.css +106 -0
  69. package/src/renderer/src-web/css/settings.css +158 -0
  70. package/src/renderer/src-web/css/styles.css +353 -1
  71. package/src/renderer/src-web/css/veo-studio.css +650 -0
  72. package/src/renderer/src-web/index.css +1 -0
  73. package/src/renderer/src-web/tauri.ts +135 -16
  74. package/src-tauri/src/lib.rs +114 -8
  75. package/src-tauri/src/webhooks.rs +45 -31
  76. package/src-tauri/src/workflows.rs +1 -0
  77. package/src-tauri/tauri.conf.json +1 -1
  78. package/temp_gridgeist/AGENTS.md +8 -0
  79. package/temp_gridgeist/CHANGELOG.md +130 -0
  80. package/temp_gridgeist/LICENSE +21 -0
  81. package/temp_gridgeist/README.md +323 -0
  82. package/temp_gridgeist/README.th.md +325 -0
  83. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  84. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  85. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  86. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  87. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  88. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  89. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  90. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -57,6 +57,40 @@ export async function getRuntimeStatus(): Promise<RuntimeStatus> {
57
57
  return invoke<RuntimeStatus>('get_runtime_status')
58
58
  }
59
59
 
60
+ export async function setActiveModel(provider: string, model?: string): Promise<string> {
61
+ if (typeof window === 'undefined' || !isTauriRuntime()) {
62
+ const API_BASE = getApiBase()
63
+ try {
64
+ const res = await fetch(`${API_BASE}/active-model`, {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({ provider, model }),
68
+ })
69
+ const data = await res.json()
70
+ return data.displayName || `Changed model to ${provider}`
71
+ } catch (e) {
72
+ console.error('Failed to set active model via API:', e)
73
+ return `Changed model to ${provider}`
74
+ }
75
+ }
76
+ const { invoke } = await import('@tauri-apps/api/core')
77
+ return invoke<string>('set_active_model', { provider, model })
78
+ }
79
+
80
+ export async function uploadFile(file: File): Promise<string> {
81
+ const API_BASE = getApiBase();
82
+ const res = await fetch(`${API_BASE}/uploads?filename=${encodeURIComponent(file.name)}`, {
83
+ method: 'POST',
84
+ body: file,
85
+ });
86
+ if (!res.ok) {
87
+ const data = await res.json().catch(() => null);
88
+ throw new Error(data?.error || `HTTP ${res.status}`);
89
+ }
90
+ const data = await res.json();
91
+ return data.url;
92
+ }
93
+
60
94
  export interface DetectedTools {
61
95
  docker: boolean
62
96
  git: boolean
@@ -83,19 +117,20 @@ export async function sendChatMessage(
83
117
  message: string,
84
118
  imageDataUri?: string | null,
85
119
  audioDataUri?: string | null,
120
+ videoDataUri?: string | null,
86
121
  documentAttachment?: DocumentAttachment | null,
87
122
  workspacePath?: string | null,
88
123
  chatId?: string | null,
89
124
  agentId?: string | null,
90
125
  ): Promise<ChatResponse> {
91
- const outgoingMessage = withImagePlaceholder(message, imageDataUri)
126
+ const outgoingMessage = withImagePlaceholder(message, imageDataUri, videoDataUri)
92
127
  if (typeof window === 'undefined' || !isTauriRuntime()) {
93
128
  const API_BASE = getApiBase();
94
129
  try {
95
130
  const res = await fetch(`${API_BASE}/chat`, {
96
131
  method: 'POST',
97
132
  headers: { 'Content-Type': 'application/json' },
98
- body: JSON.stringify({ message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, documentAttachment, agentId })
133
+ body: JSON.stringify({ message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, agentId })
99
134
  });
100
135
  const data = await res.json().catch(() => null);
101
136
  if (!res.ok) {
@@ -120,7 +155,7 @@ export async function sendChatMessage(
120
155
  }
121
156
  const { invoke } = await import('@tauri-apps/api/core')
122
157
  const response = await invoke<ChatResponse>('send_chat_message', {
123
- request: { message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, documentAttachment, workspacePath, agentId },
158
+ request: { message: outgoingMessage, systemInstruction: '', chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, workspacePath, agentId },
124
159
  })
125
160
  if (imageDataUri) {
126
161
  await invoke('save_pictures', {
@@ -129,6 +164,13 @@ export async function sendChatMessage(
129
164
  message: outgoingMessage,
130
165
  })
131
166
  }
167
+ if (videoDataUri) {
168
+ await invoke('save_pictures', {
169
+ images: videoDataUri.split(' '),
170
+ source: 'chat',
171
+ message: outgoingMessage,
172
+ })
173
+ }
132
174
  return response
133
175
  }
134
176
 
@@ -137,6 +179,7 @@ export async function streamChatMessage(
137
179
  onChunk: (chunk: string) => void,
138
180
  imageDataUri?: string | null,
139
181
  audioDataUri?: string | null,
182
+ videoDataUri?: string | null,
140
183
  systemInstruction = '',
141
184
  onProgress?: (progress: AgentProgress) => void,
142
185
  documentAttachment?: DocumentAttachment | null,
@@ -146,11 +189,11 @@ export async function streamChatMessage(
146
189
  ): Promise<ChatResponse> {
147
190
  if (typeof window === 'undefined' || !isTauriRuntime()) {
148
191
  const API_BASE = getApiBase();
149
- const outgoingMessage = withImagePlaceholder(message, imageDataUri);
192
+ const outgoingMessage = withImagePlaceholder(message, imageDataUri, videoDataUri);
150
193
  const res = await fetch(`${API_BASE}/chat-stream`, {
151
194
  method: 'POST',
152
195
  headers: { 'Content-Type': 'application/json' },
153
- body: JSON.stringify({ message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, documentAttachment, agentId })
196
+ body: JSON.stringify({ message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, agentId })
154
197
  });
155
198
  if (!res.ok) {
156
199
  const data = await res.json().catch(() => null);
@@ -189,14 +232,14 @@ export async function streamChatMessage(
189
232
  throw new Error("Stream closed without a final response");
190
233
  }
191
234
  const { invoke, Channel } = await import('@tauri-apps/api/core')
192
- const outgoingMessage = withImagePlaceholder(message, imageDataUri)
235
+ const outgoingMessage = withImagePlaceholder(message, imageDataUri, videoDataUri)
193
236
  const onEvent = new Channel<DesktopStreamEvent>()
194
237
  onEvent.onmessage = (event) => {
195
238
  if (event.type === 'chunk') onChunk(event.chunk)
196
239
  else onProgress?.(event.progress)
197
240
  }
198
241
  const response = await invoke<ChatResponse>('stream_chat_message', {
199
- request: { message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, documentAttachment, workspacePath, agentId },
242
+ request: { message: outgoingMessage, systemInstruction, chatId, imageDataUri, audioDataUri, videoDataUri, documentAttachment, workspacePath, agentId },
200
243
  onEvent,
201
244
  })
202
245
  if (imageDataUri) {
@@ -206,14 +249,29 @@ export async function streamChatMessage(
206
249
  message: outgoingMessage,
207
250
  })
208
251
  }
252
+ if (videoDataUri) {
253
+ await invoke('save_pictures', {
254
+ images: videoDataUri.split(' '),
255
+ source: 'chat',
256
+ message: outgoingMessage,
257
+ })
258
+ }
209
259
  return response
210
260
  }
211
261
 
212
- function withImagePlaceholder(message: string, imageDataUri?: string | null) {
213
- if (!imageDataUri || message.includes('[Image #1]')) return message
214
- const imageCount = imageDataUri.split(/\s+/).filter(Boolean).length
215
- const markers = Array.from({ length: imageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
216
- return markers ? `${message} ${markers}` : message
262
+ function withImagePlaceholder(message: string, imageDataUri?: string | null, videoDataUri?: string | null) {
263
+ let finalMessage = message
264
+ if (imageDataUri && !finalMessage.includes('[Image #1]')) {
265
+ const imageCount = imageDataUri.split(/\s+/).filter(Boolean).length
266
+ const markers = Array.from({ length: imageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
267
+ if (markers) finalMessage = `${finalMessage} ${markers}`
268
+ }
269
+ if (videoDataUri && !finalMessage.includes('[Video #1]')) {
270
+ const videoCount = videoDataUri.split(/\s+/).filter(Boolean).length
271
+ const markers = Array.from({ length: videoCount }, (_, index) => `[Video #${index + 1}]`).join(' ')
272
+ if (markers) finalMessage = `${finalMessage} ${markers}`
273
+ }
274
+ return finalMessage
217
275
  }
218
276
 
219
277
  export async function getTtsUrls(text: string): Promise<TtsUrl[]> {
@@ -257,6 +315,7 @@ export async function getRecentInteractions(limit = 50, chatId?: string | null):
257
315
  export async function saveSystemInteraction(
258
316
  chatId: string,
259
317
  userText: string,
318
+ aiText: string,
260
319
  provider: string,
261
320
  model: string,
262
321
  ): Promise<any> {
@@ -266,7 +325,7 @@ export async function saveSystemInteraction(
266
325
  const res = await fetch(`${API_BASE}/interactions`, {
267
326
  method: 'POST',
268
327
  headers: { 'Content-Type': 'application/json' },
269
- body: JSON.stringify({ chatId, userText, provider, model }),
328
+ body: JSON.stringify({ chatId, userText, aiText, provider, model }),
270
329
  });
271
330
  return await res.json();
272
331
  } catch (e) {
@@ -275,7 +334,7 @@ export async function saveSystemInteraction(
275
334
  }
276
335
  }
277
336
  const { invoke } = await import('@tauri-apps/api/core')
278
- return invoke('save_system_interaction', { chatId, userText, provider, model })
337
+ return invoke('save_system_interaction', { chatId, userText, aiText, provider, model })
279
338
  }
280
339
 
281
340
  export async function saveInteractionAgentActivity(
@@ -972,7 +1031,7 @@ export function installTauriAdapters() {
972
1031
  }
973
1032
 
974
1033
  window.api = {
975
- sendMessage: (message, imageDataUri, audioDataUri, documentAttachment) => sendChatMessage(message, imageDataUri, audioDataUri, documentAttachment),
1034
+ sendMessage: (message, imageDataUri, audioDataUri, documentAttachment) => sendChatMessage(message, imageDataUri, audioDataUri, undefined, documentAttachment),
976
1035
  closeWindow: async () => {
977
1036
  const { invoke } = await import('@tauri-apps/api/core')
978
1037
  const { getCurrentWindow } = await import('@tauri-apps/api/window')
@@ -1119,13 +1178,18 @@ async function captureSharedScreen(): Promise<string> {
1119
1178
  export async function listLearnedSkills(workspacePath?: string): Promise<LearnedSkill[]> {
1120
1179
  if (typeof window === 'undefined' || !isTauriRuntime()) {
1121
1180
  try {
1181
+ const API_BASE = getLocalApiBase()
1182
+ const res = await fetch(`${API_BASE}/learned-skills`)
1183
+ if (res.ok) {
1184
+ return await res.json()
1185
+ }
1122
1186
  const val = await getProfileValue('learned-skills-web-mock')
1123
1187
  if (val) {
1124
1188
  return JSON.parse(val)
1125
1189
  }
1126
1190
  return []
1127
1191
  } catch (e) {
1128
- console.error("Failed to load web mock skills:", e)
1192
+ console.error("Failed to load web skills:", e)
1129
1193
  return []
1130
1194
  }
1131
1195
  }
@@ -1230,6 +1294,61 @@ export async function readClipboardImage(): Promise<string | null> {
1230
1294
  }
1231
1295
  }
1232
1296
 
1297
+ // ─── Veo Studio — Video Generation (stub; real Tauri command in follow-up) ───
1298
+
1299
+ export interface VideoGenRequest {
1300
+ prompt: string
1301
+ negativePrompt?: string
1302
+ aspectRatio: '16:9' | '9:16' | '1:1'
1303
+ duration: 5 | 8
1304
+ model?: string
1305
+ provider: string
1306
+ }
1307
+
1308
+ export interface VideoGenEntry {
1309
+ id: string
1310
+ url: string
1311
+ path: string
1312
+ message?: string
1313
+ createdAt?: string
1314
+ }
1315
+
1316
+ export interface VideoGenResponse {
1317
+ videos: VideoGenEntry[]
1318
+ provider: string
1319
+ model: string
1320
+ description?: string
1321
+ }
1322
+
1323
+ export interface VideoGenProviders {
1324
+ active: string
1325
+ available: string[]
1326
+ }
1327
+
1328
+ /**
1329
+ * Generate a video using the configured provider.
1330
+ * NOTE: This is a frontend stub — the real Tauri backend command will be
1331
+ * added in a follow-up PR once the Veo REST API integration is ready.
1332
+ */
1333
+ export async function generateVideo(request: VideoGenRequest): Promise<VideoGenResponse> {
1334
+ const apiBase = getLocalApiBase()
1335
+ const response = await fetch(`${apiBase}/video-generate`, {
1336
+ method: 'POST',
1337
+ headers: { 'Content-Type': 'application/json' },
1338
+ body: JSON.stringify(request)
1339
+ })
1340
+ if (!response.ok) {
1341
+ const errorData = await response.json().catch(() => ({}))
1342
+ throw new Error(errorData.error || `HTTP ${response.status} from API`)
1343
+ }
1344
+ return response.json()
1345
+ }
1346
+
1347
+ export async function getVideoGenProviders(): Promise<VideoGenProviders> {
1348
+ return { active: 'veo', available: ['veo'] }
1349
+ }
1350
+
1351
+
1233
1352
  // Enforce compile-time check against the shared platform interface
1234
1353
  const _apiCheck: MintPlatformApi = {
1235
1354
  getRuntimeStatus,
@@ -31,11 +31,11 @@ use integrations::{channel_inventory, list_plugins};
31
31
  use mint_core::{
32
32
  AgentApproval, AgentProgress, AppliedCodeEdit, ApprovalOutcome, ChatRequest, ChatResponse,
33
33
  ChatSession, CodeEdit, CodeEditProposal, ImageGenRequest, InteractionMemory, MemoryStore,
34
- MintConfig, PictureEntry, TtsUrl, WeatherReport, apply_code_edits, classify_shell_command,
35
- config_path, google_tts_urls, list_saved_pictures, load_config, load_workflows,
36
- orchestrate_agent_loop, orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
37
- propose_code_edits, save_chat_images, save_config, save_workflows, start_channels, weather,
38
- workflows_path,
34
+ MintConfig, PictureEntry, TtsUrl, VideoGenRequest, VideoGenResponse, WeatherReport,
35
+ apply_code_edits, classify_shell_command, config_path, google_tts_urls, list_saved_pictures,
36
+ load_config, load_workflows, orchestrate_agent_loop, orchestrate_chat_stream_with_fallback,
37
+ orchestrate_chat_with_fallback, propose_code_edits, save_chat_images, save_config,
38
+ save_workflows, start_channels, weather, workflows_path,
39
39
  };
40
40
  use plugins::execute_plugin;
41
41
 
@@ -73,6 +73,7 @@ struct RuntimeStatus {
73
73
  backend: &'static str,
74
74
  config_path: String,
75
75
  active_provider: String,
76
+ active_model: String,
76
77
  available_providers: Vec<String>,
77
78
  integrations: Value,
78
79
  }
@@ -119,6 +120,7 @@ fn get_runtime_status() -> Result<RuntimeStatus, String> {
119
120
  .display()
120
121
  .to_string(),
121
122
  active_provider: config.ai_provider.clone(),
123
+ active_model: config.active_model().to_string(),
122
124
  available_providers: config.available_providers(),
123
125
  integrations: integration_status(&config),
124
126
  })
@@ -305,6 +307,20 @@ fn update_config(app: AppHandle, config: MintConfig) -> Result<(), String> {
305
307
  Ok(())
306
308
  }
307
309
 
310
+ #[tauri::command]
311
+ fn set_active_model(
312
+ app: AppHandle,
313
+ provider: String,
314
+ model: Option<String>,
315
+ ) -> Result<String, String> {
316
+ let mut config = load_config().map_err(|error| error.to_string())?;
317
+ let display_name = config
318
+ .set_active_model(&provider, model.as_deref())
319
+ .map_err(|error| error.to_string())?;
320
+ let _ = app.emit("settings-changed", &config);
321
+ Ok(display_name)
322
+ }
323
+
308
324
  #[tauri::command]
309
325
  fn inspect_shell_command(command: String) -> mint_core::ShellClassification {
310
326
  classify_shell_command(&command)
@@ -312,7 +328,21 @@ fn inspect_shell_command(command: String) -> mint_core::ShellClassification {
312
328
 
313
329
  #[tauri::command]
314
330
  async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatResponse, String> {
315
- let config = load_config().map_err(|error| error.to_string())?;
331
+ let mut config = load_config().map_err(|error| error.to_string())?;
332
+ if let Some(ref path) = request.workspace_path {
333
+ if !path.trim().is_empty()
334
+ && config
335
+ .extra
336
+ .get("activeWorkspacePath")
337
+ .and_then(Value::as_str)
338
+ != Some(path.as_str())
339
+ {
340
+ config
341
+ .extra
342
+ .insert("activeWorkspacePath".into(), Value::String(path.clone()));
343
+ let _ = save_config(&config);
344
+ }
345
+ }
316
346
  let request = request.with_document_context(&config)?;
317
347
 
318
348
  if request.message.starts_with("/chat ") {
@@ -392,6 +422,7 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
392
422
  let message_clone = request.message.clone();
393
423
  let root_clone = root.clone();
394
424
  let image_data_uri_clone = request.image_data_uri.clone();
425
+ let video_data_uri_clone = request.video_data_uri.clone();
395
426
  let chat_id_clone = request.chat_id.clone();
396
427
  let agent_id_clone = request.agent_id.clone();
397
428
 
@@ -401,6 +432,7 @@ async fn send_chat_message(app: AppHandle, request: ChatRequest) -> Result<ChatR
401
432
  &message_clone,
402
433
  &root_clone,
403
434
  image_data_uri_clone,
435
+ video_data_uri_clone,
404
436
  chat_id_clone.as_deref(),
405
437
  agent_id_clone.as_deref(),
406
438
  fast_mode,
@@ -449,7 +481,21 @@ async fn stream_chat_message(
449
481
  request: ChatRequest,
450
482
  on_event: Channel<DesktopStreamEvent>,
451
483
  ) -> Result<ChatResponse, String> {
452
- let config = load_config().map_err(|error| error.to_string())?;
484
+ let mut config = load_config().map_err(|error| error.to_string())?;
485
+ if let Some(ref path) = request.workspace_path {
486
+ if !path.trim().is_empty()
487
+ && config
488
+ .extra
489
+ .get("activeWorkspacePath")
490
+ .and_then(Value::as_str)
491
+ != Some(path.as_str())
492
+ {
493
+ config
494
+ .extra
495
+ .insert("activeWorkspacePath".into(), Value::String(path.clone()));
496
+ let _ = save_config(&config);
497
+ }
498
+ }
453
499
  let request = request.with_document_context(&config)?;
454
500
 
455
501
  if request.message.starts_with("/chat ") {
@@ -548,6 +594,7 @@ async fn stream_chat_message(
548
594
  let message_clone = request.message.clone();
549
595
  let root_clone = root.clone();
550
596
  let image_data_uri_clone = request.image_data_uri.clone();
597
+ let video_data_uri_clone = request.video_data_uri.clone();
551
598
  let chat_id_clone = request.chat_id.clone();
552
599
  let agent_id_clone = request.agent_id.clone();
553
600
 
@@ -557,6 +604,7 @@ async fn stream_chat_message(
557
604
  &message_clone,
558
605
  &root_clone,
559
606
  image_data_uri_clone,
607
+ video_data_uri_clone,
560
608
  chat_id_clone.as_deref(),
561
609
  agent_id_clone.as_deref(),
562
610
  fast_mode,
@@ -624,12 +672,19 @@ fn save_interaction_agent_activity(
624
672
  fn save_system_interaction(
625
673
  chat_id: String,
626
674
  user_text: String,
675
+ ai_text: Option<String>,
627
676
  provider: String,
628
677
  model: String,
629
678
  ) -> Result<i64, String> {
630
679
  MemoryStore::open_default()
631
680
  .and_then(|memory| {
632
- memory.add_interaction_for_chat(&chat_id, &user_text, "", &provider, &model)
681
+ memory.add_interaction_for_chat(
682
+ &chat_id,
683
+ &user_text,
684
+ ai_text.as_deref().unwrap_or(""),
685
+ &provider,
686
+ &model,
687
+ )
633
688
  })
634
689
  .map_err(|error| error.to_string())
635
690
  }
@@ -704,6 +759,9 @@ fn list_learned_skills(workspace_path: Option<String>) -> Result<Vec<LearnedSkil
704
759
 
705
760
  let mut global_skills = Vec::new();
706
761
  if let Some(home) = dirs::home_dir() {
762
+ let global_agents_path = home.join(".gemini").join("config").join("AGENTS.md");
763
+ mint_core::skills::load_agent_rules_file(&global_agents_path, &mut global_skills);
764
+
707
765
  let global_skills_path = home.join(".config").join("mint").join("mint-skills");
708
766
  if !global_skills_path.exists() {
709
767
  let _ = std::fs::create_dir_all(&global_skills_path);
@@ -713,6 +771,12 @@ fn list_learned_skills(workspace_path: Option<String>) -> Result<Vec<LearnedSkil
713
771
 
714
772
  let mut workspace_skills = Vec::new();
715
773
  if let Ok(root) = workspace_root(workspace_path.as_deref()) {
774
+ let workspace_agents_path1 = root.join(".agents").join("AGENTS.md");
775
+ mint_core::skills::load_agent_rules_file(&workspace_agents_path1, &mut workspace_skills);
776
+
777
+ let workspace_agents_path2 = root.join("AGENTS.md");
778
+ mint_core::skills::load_agent_rules_file(&workspace_agents_path2, &mut workspace_skills);
779
+
716
780
  let workspace_skills_path1 = root.join(".agents").join("skills");
717
781
  mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut workspace_skills);
718
782
 
@@ -867,6 +931,14 @@ async fn generate_images(request: ImageGenRequest) -> Result<DesktopImageGenResp
867
931
  })
868
932
  }
869
933
 
934
+ #[tauri::command]
935
+ async fn generate_video(request: VideoGenRequest) -> Result<VideoGenResponse, String> {
936
+ let config = load_config().map_err(|error| error.to_string())?;
937
+ mint_core::generate_video(&config, &request)
938
+ .await
939
+ .map_err(|error| error.to_string())
940
+ }
941
+
870
942
  #[tauri::command]
871
943
  fn list_pictures() -> Result<Vec<PictureEntry>, String> {
872
944
  list_saved_pictures().map_err(|error| error.to_string())
@@ -881,6 +953,37 @@ fn save_pictures(
881
953
  save_chat_images(images, source, message).map_err(|error| error.to_string())
882
954
  }
883
955
 
956
+ #[tauri::command]
957
+ fn upload_file(filename: String, data_b64: String) -> Result<String, String> {
958
+ // Determine mime from filename
959
+ let mime = filename
960
+ .rsplit_once('.')
961
+ .map(|(_, ext)| match ext.to_ascii_lowercase().as_str() {
962
+ "mp4" => "video/mp4",
963
+ "webm" => "video/webm",
964
+ "mov" => "video/quicktime",
965
+ "mkv" => "video/x-matroska",
966
+ "avi" => "video/x-msvideo",
967
+ _ => "application/octet-stream",
968
+ })
969
+ .unwrap_or("application/octet-stream");
970
+
971
+ let data_uri = format!("data:{};base64,{}", mime, data_b64);
972
+ match save_chat_images(
973
+ vec![data_uri],
974
+ Some("upload".into()),
975
+ Some("uploaded".into()),
976
+ ) {
977
+ Ok(mut saved) => {
978
+ if let Some(entry) = saved.pop() {
979
+ return Ok(format!("/api/pictures/{}", entry.filename));
980
+ }
981
+ Err("no entry saved".into())
982
+ }
983
+ Err(e) => Err(e.to_string()),
984
+ }
985
+ }
986
+
884
987
  #[tauri::command]
885
988
  fn open_folder(path: String) -> Result<ActionResult, String> {
886
989
  let target_path = PathBuf::from(path.trim());
@@ -1241,12 +1344,14 @@ pub fn run() {
1241
1344
  create_workspace_folder,
1242
1345
  delete_workspace_item,
1243
1346
  generate_images,
1347
+ generate_video,
1244
1348
  select_workspace_directory,
1245
1349
  get_config,
1246
1350
  get_updater_status,
1247
1351
  check_for_updates,
1248
1352
  install_available_update,
1249
1353
  update_config,
1354
+ set_active_model,
1250
1355
  inspect_shell_command,
1251
1356
  send_chat_message,
1252
1357
  stream_chat_message,
@@ -1265,6 +1370,7 @@ pub fn run() {
1265
1370
  delete_learned_skill,
1266
1371
  list_pictures,
1267
1372
  save_pictures,
1373
+ upload_file,
1268
1374
  open_folder,
1269
1375
  get_tts_urls,
1270
1376
  get_weather,
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
2
2
 
3
3
  use base64::{Engine as _, engine::general_purpose::STANDARD};
4
4
  use hmac::{Hmac, Mac};
5
- use mint_core::{ChatRequest, load_config, orchestrate_chat};
5
+ use mint_core::load_config;
6
6
  use serde_json::{Value, json};
7
7
  use sha2::Sha256;
8
8
  use tokio::{
@@ -14,10 +14,16 @@ type HmacSha256 = Hmac<Sha256>;
14
14
 
15
15
  pub fn start_webhooks() {
16
16
  tauri::async_runtime::spawn(async {
17
- let _ = serve(3000, Service::Line).await;
17
+ let (host, port) = load_config()
18
+ .map(|c| (c.line_webhook_host(), c.line_webhook_port()))
19
+ .unwrap_or_else(|_| ("127.0.0.1".into(), 3000));
20
+ let _ = serve(&host, port, Service::Line).await;
18
21
  });
19
22
  tauri::async_runtime::spawn(async {
20
- let _ = serve(3001, Service::Whatsapp).await;
23
+ let (host, port) = load_config()
24
+ .map(|c| (c.whatsapp_webhook_host(), c.whatsapp_webhook_port()))
25
+ .unwrap_or_else(|_| ("127.0.0.1".into(), 3001));
26
+ let _ = serve(&host, port, Service::Whatsapp).await;
21
27
  });
22
28
  }
23
29
 
@@ -27,8 +33,9 @@ enum Service {
27
33
  Whatsapp,
28
34
  }
29
35
 
30
- async fn serve(port: u16, service: Service) -> Result<(), String> {
31
- let listener = TcpListener::bind(("127.0.0.1", port))
36
+ async fn serve(host: &str, port: u16, service: Service) -> Result<(), String> {
37
+ let addr = format!("{host}:{port}");
38
+ let listener = TcpListener::bind(&addr)
32
39
  .await
33
40
  .map_err(|error| error.to_string())?;
34
41
  loop {
@@ -78,9 +85,28 @@ async fn handle_line(request: HttpRequest) -> Result<(&'static str, String), Str
78
85
  ) else {
79
86
  continue;
80
87
  };
81
- let answer = answer(text, "Reply concisely for a LINE chat.").await;
82
- let _ = mint_core::HTTP_CLIENT.clone().post("https://api.line.me/v2/bot/message/reply").bearer_auth(&token)
83
- .json(&json!({ "replyToken": reply_token, "messages": [{ "type": "text", "text": answer }] })).send().await;
88
+ let user_id = event["source"]["userId"].as_str().unwrap_or("unknown");
89
+ let formatted_chat_id = format!("line:{user_id}");
90
+ let answer = mint_core::channels::answer_channel(
91
+ text,
92
+ "Reply concisely for a LINE chat.",
93
+ Some(formatted_chat_id),
94
+ )
95
+ .await;
96
+
97
+ let mut messages = Vec::new();
98
+ if config.bridge_ack_enabled() {
99
+ messages.push(json!({ "type": "text", "text": config.bridge_ack_message() }));
100
+ }
101
+ messages.push(json!({ "type": "text", "text": answer }));
102
+
103
+ let _ = mint_core::HTTP_CLIENT
104
+ .clone()
105
+ .post("https://api.line.me/v2/bot/message/reply")
106
+ .bearer_auth(&token)
107
+ .json(&json!({ "replyToken": reply_token, "messages": messages }))
108
+ .send()
109
+ .await;
84
110
  }
85
111
  Ok(("200 OK", "ok".into()))
86
112
  }
@@ -133,35 +159,23 @@ async fn handle_whatsapp(request: HttpRequest) -> Result<(&'static str, String),
133
159
  else {
134
160
  continue;
135
161
  };
136
- let answer = answer(text, "Reply concisely for a WhatsApp chat.").await;
162
+ if config.bridge_ack_enabled() {
163
+ let _ = mint_core::HTTP_CLIENT.clone().post(format!("https://graph.facebook.com/v23.0/{phone_id}/messages")).bearer_auth(&access_token)
164
+ .json(&json!({ "messaging_product": "whatsapp", "to": to, "type": "text", "text": { "body": config.bridge_ack_message() } })).send().await;
165
+ }
166
+ let formatted_chat_id = format!("whatsapp:{to}");
167
+ let answer = mint_core::channels::answer_channel(
168
+ text,
169
+ "Reply concisely for a WhatsApp chat.",
170
+ Some(formatted_chat_id),
171
+ )
172
+ .await;
137
173
  let _ = mint_core::HTTP_CLIENT.clone().post(format!("https://graph.facebook.com/v23.0/{phone_id}/messages")).bearer_auth(&access_token)
138
174
  .json(&json!({ "messaging_product": "whatsapp", "to": to, "type": "text", "text": { "body": answer } })).send().await;
139
175
  }
140
176
  Ok(("200 OK", "ok".into()))
141
177
  }
142
178
 
143
- async fn answer(text: &str, system: &str) -> String {
144
- let Ok(config) = load_config() else {
145
- return "Mint config error".into();
146
- };
147
- orchestrate_chat(
148
- &config,
149
- &ChatRequest {
150
- message: text.into(),
151
- system_instruction: system.into(),
152
- chat_id: None,
153
- image_data_uri: None,
154
- audio_data_uri: None,
155
- document_attachment: None,
156
- workspace_path: None,
157
- agent_id: None,
158
- },
159
- )
160
- .await
161
- .map(|response| response.text)
162
- .unwrap_or_else(|error| format!("Mint error: {error}"))
163
- }
164
-
165
179
  struct HttpRequest {
166
180
  method: String,
167
181
  path: String,
@@ -79,6 +79,7 @@ pub fn start_monitor(app: AppHandle) {
79
79
  chat_id: None,
80
80
  image_data_uri: None,
81
81
  audio_data_uri: None,
82
+ video_data_uri: None,
82
83
  document_attachment: None,
83
84
  workspace_path: None,
84
85
  agent_id: None,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Mint",
4
- "version": "1.9.0",
4
+ "version": "1.10.0",
5
5
  "identifier": "com.pheem49.mint",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run dev:desktop",
@@ -0,0 +1,8 @@
1
+ # Project instructions
2
+
3
+ - Do not use any `superpowers:*` skills in this repository.
4
+ - Do not create or restore files under `docs/superpowers/`.
5
+ - Use the `solodeveling` skill at the start of every task in this repository. Resume `.solodeveling/state.md` when present and let the skill route the work to the smallest relevant lifecycle workflow.
6
+ - Keep one primary agent. Do not use subagents unless the user explicitly requests delegation.
7
+ - If `solodeveling` is unavailable, state that briefly and continue with the same evidence-driven lifecycle rather than blocking routine work.
8
+ - Work directly from the user's request and use only the minimum additional relevant skills.