@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
@@ -4,6 +4,7 @@ use std::{
4
4
  process::{Command, Stdio},
5
5
  };
6
6
 
7
+ use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
7
8
  use serde::Deserialize;
8
9
  use serde_json::{Value, json};
9
10
  use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -11,23 +12,48 @@ use tokio::net::TcpListener;
11
12
 
12
13
  use crate::{
13
14
  AgentProgress, ApprovalOutcome, ChatRequest, ChatResponse, DEFAULT_CONVERSATION_ID,
14
- ImageGenRequest, MemoryStore, MintConfig, config_path, create_folder, find_paths,
15
- generate_images, list_saved_pictures, load_config, orchestrate_agent_loop,
16
- orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback, save_chat_images,
17
- save_config, weather,
15
+ ImageGenRequest, MemoryStore, MintConfig, VideoGenRequest, config_path, create_folder,
16
+ find_paths, generate_images, generate_video, list_saved_pictures, load_config,
17
+ orchestrate_agent_loop, orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback,
18
+ save_chat_images, save_config, weather,
18
19
  };
19
20
 
20
21
  const MAX_API_REQUEST_BYTES: usize = 32 * 1024 * 1024;
21
22
 
23
+ pub(crate) fn log_api_req(method: &str, route: &str, status: &str, info: Option<&str>) {
24
+ let timestamp = chrono::Local::now().format("%H:%M:%S");
25
+ let status_str = if status.starts_with('2') {
26
+ format!("\x1b[32m{}\x1b[0m", status)
27
+ } else if status.starts_with('4') || status.starts_with('5') {
28
+ format!("\x1b[1;31m{}\x1b[0m", status)
29
+ } else {
30
+ format!("\x1b[33m{}\x1b[0m", status)
31
+ };
32
+ if let Some(detail) = info {
33
+ println!(
34
+ "\x1b[90m[{}]\x1b[0m \x1b[1;36m[API]\x1b[0m \x1b[1m{}\x1b[0m {} -> {} \x1b[90m({})\x1b[0m",
35
+ timestamp, method, route, status_str, detail
36
+ );
37
+ } else {
38
+ println!(
39
+ "\x1b[90m[{}]\x1b[0m \x1b[1;36m[API]\x1b[0m \x1b[1m{}\x1b[0m {} -> {}",
40
+ timestamp, method, route, status_str
41
+ );
42
+ }
43
+ }
44
+
45
+ pub(crate) fn log_api_err(context: &str, error: &dyn std::fmt::Display) {
46
+ let timestamp = chrono::Local::now().format("%H:%M:%S");
47
+ eprintln!(
48
+ "\x1b[90m[{}]\x1b[0m \x1b[1;31m[ERROR]\x1b[0m \x1b[1;33m{}\x1b[0m: {}",
49
+ timestamp, context, error
50
+ );
51
+ }
52
+
22
53
  pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
23
54
  let addr = SocketAddr::from(([0, 0, 0, 0], port));
24
55
  let listener = TcpListener::bind(addr).await?;
25
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
26
- println!(
27
- "\x1b[32m Mint Local API Server running at http://{}\x1b[0m",
28
- addr
29
- );
30
- println!("\x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m");
56
+ // API server banner removed to prevent duplicate output
31
57
 
32
58
  // Start background messaging bridges (Telegram, Discord, Slack)
33
59
  crate::start_channels();
@@ -125,6 +151,19 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
125
151
 
126
152
  let (route, query) = path.split_once('?').unwrap_or((path, ""));
127
153
 
154
+ if route.starts_with("/api/")
155
+ && !route.starts_with("/api/pictures/")
156
+ && !route.starts_with("/api/thumbnails/")
157
+ && route != "/api/chat"
158
+ && route != "/api/chat-stream"
159
+ && route != "/api/image-generate"
160
+ && route != "/api/video-generate"
161
+ && route != "/api/action"
162
+ && route != "/api/config"
163
+ {
164
+ log_api_req(method, route, "200 OK", None);
165
+ }
166
+
128
167
  match (method, route) {
129
168
  ("GET", "/api/status") => {
130
169
  let config = load_config().unwrap_or_default();
@@ -230,6 +269,48 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
230
269
  )
231
270
  .await;
232
271
  }
272
+ ("GET", "/api/learned-skills") => {
273
+ let mut skills = match MemoryStore::open_default() {
274
+ Ok(m) => m.learned_skills(100).unwrap_or_default(),
275
+ Err(_) => Vec::new(),
276
+ };
277
+
278
+ if let Some(home) = dirs::home_dir() {
279
+ let global_agents_path =
280
+ home.join(".gemini").join("config").join("AGENTS.md");
281
+ crate::skills::load_agent_rules_file(&global_agents_path, &mut skills);
282
+
283
+ let global_skills_path =
284
+ home.join(".config").join("mint").join("mint-skills");
285
+ crate::skills::load_skills_from_dir(&global_skills_path, &mut skills);
286
+ }
287
+
288
+ if let Ok(current_dir) = std::env::current_dir() {
289
+ let workspace_agents_path1 = current_dir.join(".agents").join("AGENTS.md");
290
+ crate::skills::load_agent_rules_file(&workspace_agents_path1, &mut skills);
291
+ let workspace_agents_path2 = current_dir.join("AGENTS.md");
292
+ crate::skills::load_agent_rules_file(&workspace_agents_path2, &mut skills);
293
+
294
+ let workspace_skills_path1 = current_dir.join(".agents").join("skills");
295
+ crate::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
296
+ let workspace_skills_path2 = current_dir.join("skills");
297
+ crate::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
298
+ }
299
+
300
+ let mut unique_skills = std::collections::BTreeMap::new();
301
+ for s in skills {
302
+ unique_skills.insert(s.name.clone(), s);
303
+ }
304
+
305
+ let list: Vec<_> = unique_skills.into_values().collect();
306
+ send_json_response(
307
+ socket,
308
+ "200 OK",
309
+ &serde_json::to_string(&list).unwrap_or_default(),
310
+ )
311
+ .await;
312
+ return;
313
+ }
233
314
  ("GET", "/api/profile") => {
234
315
  let key = query_param(query, "key").unwrap_or_default();
235
316
  if let Ok(memory) = MemoryStore::open_default() {
@@ -265,6 +346,35 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
265
346
  )
266
347
  .await;
267
348
  }
349
+ ("POST", "/api/active-model") => {
350
+ #[derive(Deserialize)]
351
+ struct ActiveModelReq {
352
+ provider: String,
353
+ model: Option<String>,
354
+ }
355
+ if let Ok(req) = serde_json::from_str::<ActiveModelReq>(body)
356
+ && let Ok(mut config) = load_config()
357
+ {
358
+ if let Ok(display_name) =
359
+ config.set_active_model(&req.provider, req.model.as_deref())
360
+ {
361
+ send_json_response(
362
+ socket,
363
+ "200 OK",
364
+ &serde_json::json!({ "status": "ok", "displayName": display_name })
365
+ .to_string(),
366
+ )
367
+ .await;
368
+ return;
369
+ }
370
+ }
371
+ send_json_response(
372
+ socket,
373
+ "500 Internal Server Error",
374
+ "{\"status\":\"error\"}",
375
+ )
376
+ .await;
377
+ }
268
378
  ("POST", "/api/interactions/clear") => {
269
379
  if let Ok(memory) = MemoryStore::open_default() {
270
380
  let chat_id = query_param(query, "chatId")
@@ -286,6 +396,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
286
396
  struct ApiSaveInteraction {
287
397
  chat_id: String,
288
398
  user_text: String,
399
+ ai_text: Option<String>,
289
400
  provider: String,
290
401
  model: String,
291
402
  }
@@ -295,7 +406,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
295
406
  match memory.add_interaction_for_chat(
296
407
  &req.chat_id,
297
408
  &req.user_text,
298
- "",
409
+ req.ai_text.as_deref().unwrap_or(""),
299
410
  &req.provider,
300
411
  &req.model,
301
412
  ) {
@@ -389,6 +500,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
389
500
  }
390
501
  }
391
502
  }
503
+ ("GET", route) if route.starts_with("/api/thumbnails/") => {
504
+ let filename = percent_decode(route.trim_start_matches("/api/thumbnails/"));
505
+ match thumbnail_bytes(&filename) {
506
+ Ok((mime_type, bytes)) => {
507
+ send_binary_response(socket, "200 OK", &mime_type, &bytes).await
508
+ }
509
+ Err(_) => {
510
+ send_json_response(
511
+ socket,
512
+ "404 Not Found",
513
+ "{\"error\":\"thumbnail not found\"}",
514
+ )
515
+ .await
516
+ }
517
+ }
518
+ }
392
519
  ("GET", "/api/config") => {
393
520
  let config = load_config().unwrap_or_default();
394
521
  if let Ok(json_str) = serde_json::to_string(&config) {
@@ -458,6 +585,71 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
458
585
  .await;
459
586
  }
460
587
  }
588
+ ("POST", "/api/uploads") => {
589
+ // Accept raw body bytes and optional filename query param.
590
+ // Body is the raw file bytes (client should POST the file as the request body).
591
+ let filename_param = query_param(query, "filename");
592
+ let header_end_idx = header_end as usize;
593
+ // extract raw bytes for body
594
+ let body_bytes = &request_bytes[header_end_idx + 4..];
595
+
596
+ if body_bytes.is_empty() {
597
+ send_json_response(socket, "400 Bad Request", "{\"error\":\"empty body\"}")
598
+ .await;
599
+ return;
600
+ }
601
+
602
+ // Determine mime type from filename or fallback to video/mp4
603
+ let (mime_type, _extension) = filename_param
604
+ .as_deref()
605
+ .and_then(|f| f.rsplit_once('.'))
606
+ .map(|(_, ext)| {
607
+ let e = ext.to_ascii_lowercase();
608
+ match e.as_str() {
609
+ "mp4" => ("video/mp4", "mp4"),
610
+ "webm" => ("video/webm", "webm"),
611
+ "mov" => ("video/quicktime", "mov"),
612
+ "mkv" => ("video/x-matroska", "mkv"),
613
+ "avi" => ("video/x-msvideo", "avi"),
614
+ _ => ("application/octet-stream", "bin"),
615
+ }
616
+ })
617
+ .unwrap_or(("video/mp4", "mp4"));
618
+
619
+ // Build data URI and reuse save_chat_images helper to persist and index file
620
+ let encoded = BASE64.encode(body_bytes);
621
+ let data_uri = format!("data:{};base64,{}", mime_type, encoded);
622
+ match save_chat_images(
623
+ vec![data_uri],
624
+ Some("upload".into()),
625
+ Some("uploaded".into()),
626
+ ) {
627
+ Ok(saved) => {
628
+ if let Some(entry) = saved.into_iter().next() {
629
+ let res_json = json!({ "url": format!("/api/pictures/{}", entry.filename), "filename": entry.filename });
630
+ send_json_response(socket, "200 OK", &res_json.to_string()).await;
631
+ return;
632
+ }
633
+ }
634
+ Err(err) => {
635
+ let err_json = json!({ "error": err.to_string() });
636
+ send_json_response(
637
+ socket,
638
+ "500 Internal Server Error",
639
+ &err_json.to_string(),
640
+ )
641
+ .await;
642
+ return;
643
+ }
644
+ }
645
+ send_json_response(
646
+ socket,
647
+ "500 Internal Server Error",
648
+ "{\"error\":\"failed to save upload\"}",
649
+ )
650
+ .await;
651
+ }
652
+
461
653
  ("POST", "/api/chat") => {
462
654
  #[derive(Deserialize)]
463
655
  #[serde(rename_all = "camelCase")]
@@ -467,6 +659,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
467
659
  chat_id: Option<String>,
468
660
  image_data_uri: Option<String>,
469
661
  audio_data_uri: Option<String>,
662
+ video_data_uri: Option<String>,
470
663
  document_attachment: Option<crate::chat::DocumentAttachment>,
471
664
  agent_id: Option<String>,
472
665
  }
@@ -479,6 +672,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
479
672
  chat_id: req.chat_id,
480
673
  image_data_uri: req.image_data_uri,
481
674
  audio_data_uri: req.audio_data_uri,
675
+ video_data_uri: req.video_data_uri,
482
676
  document_attachment: req.document_attachment,
483
677
  workspace_path: None,
484
678
  agent_id: req.agent_id,
@@ -486,6 +680,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
486
680
  let mut chat_req =
487
681
  chat_req.with_document_context(&config).unwrap_or(chat_req);
488
682
  let sent_image = chat_req.image_data_uri.clone();
683
+ let sent_video = chat_req.video_data_uri.clone();
489
684
  let sent_message = chat_req.message.clone();
490
685
 
491
686
  let response = if let Some(clean_message) =
@@ -505,6 +700,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
505
700
 
506
701
  match response {
507
702
  Ok(resp) => {
703
+ log_api_req(
704
+ "POST",
705
+ "/api/chat",
706
+ "200 OK",
707
+ Some(&format!("Model: {}", config.ai_provider)),
708
+ );
508
709
  if let Some(image) = sent_image {
509
710
  let _ = save_chat_images(
510
711
  image
@@ -512,6 +713,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
512
713
  .map(str::to_owned)
513
714
  .collect::<Vec<_>>(),
514
715
  Some("web".into()),
716
+ Some(sent_message.clone()),
717
+ );
718
+ }
719
+ if let Some(video) = sent_video {
720
+ let _ = save_chat_images(
721
+ video
722
+ .split_whitespace()
723
+ .map(str::to_owned)
724
+ .collect::<Vec<_>>(),
725
+ Some("web".into()),
515
726
  Some(sent_message),
516
727
  );
517
728
  }
@@ -521,7 +732,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
521
732
  }
522
733
  }
523
734
  Err(e) => {
524
- eprintln!("API Chat error: {:?}", e);
735
+ log_api_err("API /api/chat error", &e);
525
736
  let err_json = serde_json::json!({
526
737
  "provider": "error",
527
738
  "model": "error",
@@ -571,6 +782,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
571
782
  chat_id: Option<String>,
572
783
  image_data_uri: Option<String>,
573
784
  audio_data_uri: Option<String>,
785
+ video_data_uri: Option<String>,
574
786
  document_attachment: Option<crate::chat::DocumentAttachment>,
575
787
  agent_id: Option<String>,
576
788
  }
@@ -583,6 +795,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
583
795
  chat_id: req.chat_id,
584
796
  image_data_uri: req.image_data_uri,
585
797
  audio_data_uri: req.audio_data_uri,
798
+ video_data_uri: req.video_data_uri,
586
799
  document_attachment: req.document_attachment,
587
800
  workspace_path: None,
588
801
  agent_id: req.agent_id,
@@ -590,6 +803,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
590
803
  let mut chat_req =
591
804
  chat_req.with_document_context(&config).unwrap_or(chat_req);
592
805
  let sent_image = chat_req.image_data_uri.clone();
806
+ let sent_video = chat_req.video_data_uri.clone();
593
807
  let sent_message = chat_req.message.clone();
594
808
 
595
809
  let is_chat = if let Some(clean_message) =
@@ -666,6 +880,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
666
880
 
667
881
  match result {
668
882
  Ok((response, _)) => {
883
+ log_api_req(
884
+ "POST",
885
+ "/api/chat-stream",
886
+ "200 OK",
887
+ Some(&format!(
888
+ "Provider: {}",
889
+ config_clone.ai_provider
890
+ )),
891
+ );
669
892
  if let Ok(json_val) =
670
893
  serde_json::to_string(&serde_json::json!({
671
894
  "type": "done",
@@ -676,7 +899,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
676
899
  }
677
900
  }
678
901
  Err(e) => {
679
- eprintln!("API Chat Stream error: {:?}", e);
902
+ log_api_err("API /api/chat-stream error", &e);
680
903
  let err_json = serde_json::json!({
681
904
  "type": "done",
682
905
  "response": {
@@ -722,6 +945,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
722
945
  let chat_id_str = chat_id.clone().unwrap_or_default();
723
946
  let message = chat_req.message.clone();
724
947
  let image_data_uri = chat_req.image_data_uri.clone();
948
+ let video_data_uri = chat_req.video_data_uri.clone();
725
949
  let agent_id = chat_req.agent_id.clone();
726
950
 
727
951
  let join_handle = tokio::spawn(async move {
@@ -730,6 +954,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
730
954
  &message,
731
955
  &root,
732
956
  image_data_uri,
957
+ video_data_uri,
733
958
  chat_id.as_deref(),
734
959
  agent_id.as_deref(),
735
960
  fast_mode,
@@ -807,6 +1032,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
807
1032
  .map(str::to_owned)
808
1033
  .collect::<Vec<_>>(),
809
1034
  Some("web".into()),
1035
+ Some(sent_message.clone()),
1036
+ );
1037
+ }
1038
+ if let Some(video) = sent_video {
1039
+ let _ = save_chat_images(
1040
+ video
1041
+ .split_whitespace()
1042
+ .map(str::to_owned)
1043
+ .collect::<Vec<_>>(),
1044
+ Some("web".into()),
810
1045
  Some(sent_message),
811
1046
  );
812
1047
  }
@@ -836,6 +1071,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
836
1071
  /// Which image provider to use (overrides config.image_gen_provider).
837
1072
  #[serde(default)]
838
1073
  provider: Option<String>,
1074
+ #[serde(default)]
1075
+ image_data_uri: Option<String>,
1076
+ #[serde(default)]
1077
+ mask_data_uri: Option<String>,
1078
+ #[serde(default)]
1079
+ mode: Option<String>,
839
1080
  }
840
1081
 
841
1082
  if let Ok(req) = serde_json::from_str::<ImageGenApiRequest>(body) {
@@ -847,9 +1088,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
847
1088
  num_images: req.num_images,
848
1089
  model: req.model,
849
1090
  provider: req.provider,
1091
+ image_data_uri: req.image_data_uri,
1092
+ mask_data_uri: req.mask_data_uri,
1093
+ mode: req.mode,
850
1094
  };
851
1095
  match generate_images(&config, &gen_request).await {
852
1096
  Ok(result) => {
1097
+ log_api_req(
1098
+ "POST",
1099
+ "/api/image-generate",
1100
+ "200 OK",
1101
+ Some(&format!("Provider: {}", result.provider)),
1102
+ );
853
1103
  let data_uris: Vec<String> = result
854
1104
  .images
855
1105
  .iter()
@@ -877,6 +1127,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
877
1127
  send_json_response(socket, "200 OK", &response.to_string()).await;
878
1128
  }
879
1129
  Err(e) => {
1130
+ log_api_err("API /api/image-generate error", &e);
880
1131
  let err = json!({ "error": e.to_string() });
881
1132
  send_json_response(
882
1133
  socket,
@@ -895,6 +1146,98 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
895
1146
  .await;
896
1147
  }
897
1148
  }
1149
+ ("POST", "/api/video-generate") => {
1150
+ #[derive(serde::Deserialize)]
1151
+ #[serde(rename_all = "camelCase")]
1152
+ struct VideoGenApiRequest {
1153
+ prompt: String,
1154
+ #[serde(default)]
1155
+ negative_prompt: Option<String>,
1156
+ #[serde(default)]
1157
+ aspect_ratio: Option<String>,
1158
+ #[serde(default)]
1159
+ duration: Option<u32>,
1160
+ #[serde(default)]
1161
+ model: Option<String>,
1162
+ #[serde(default)]
1163
+ provider: Option<String>,
1164
+ }
1165
+
1166
+ if let Ok(req) = serde_json::from_str::<VideoGenApiRequest>(body) {
1167
+ let config = load_config().unwrap_or_default();
1168
+ let gen_request = VideoGenRequest {
1169
+ prompt: req.prompt.clone(),
1170
+ negative_prompt: req.negative_prompt,
1171
+ aspect_ratio: req.aspect_ratio.unwrap_or_else(|| "16:9".to_string()),
1172
+ duration: req.duration.unwrap_or(5),
1173
+ model: req.model,
1174
+ provider: req.provider.unwrap_or_else(|| "veo".to_string()),
1175
+ };
1176
+ match generate_video(&config, &gen_request).await {
1177
+ Ok(result) => {
1178
+ let mut response =
1179
+ serde_json::to_value(&result).unwrap_or(json!({}));
1180
+ if let Some(videos) =
1181
+ response.get_mut("videos").and_then(|v| v.as_array_mut())
1182
+ {
1183
+ for picture in videos {
1184
+ let filename = picture
1185
+ .get("filename")
1186
+ .and_then(|f| f.as_str())
1187
+ .map(|s| s.to_string());
1188
+ if let Some(filename) = filename {
1189
+ picture.as_object_mut().unwrap().insert(
1190
+ "url".to_string(),
1191
+ json!(format!("/api/pictures/{}", filename)),
1192
+ );
1193
+ }
1194
+ let id = picture
1195
+ .get("id")
1196
+ .and_then(|i| i.as_str())
1197
+ .map(|s| s.to_string());
1198
+ if let Some(id) = id {
1199
+ let has_thumb = picture.get("thumbnailPath").is_some()
1200
+ || picture.get("thumbnailUrl").is_some();
1201
+ if has_thumb {
1202
+ picture.as_object_mut().unwrap().insert(
1203
+ "thumbnailUrl".to_string(),
1204
+ json!(format!(
1205
+ "/api/thumbnails/{}.thumb.png",
1206
+ id
1207
+ )),
1208
+ );
1209
+ }
1210
+ }
1211
+ }
1212
+ }
1213
+ log_api_req(
1214
+ "POST",
1215
+ "/api/video-generate",
1216
+ "200 OK",
1217
+ Some(&format!("Provider: {}", result.provider)),
1218
+ );
1219
+ send_json_response(socket, "200 OK", &response.to_string()).await;
1220
+ }
1221
+ Err(e) => {
1222
+ log_api_err("API /api/video-generate error", &e);
1223
+ let err = json!({ "error": e.to_string() });
1224
+ send_json_response(
1225
+ socket,
1226
+ "500 Internal Server Error",
1227
+ &err.to_string(),
1228
+ )
1229
+ .await;
1230
+ }
1231
+ }
1232
+ } else {
1233
+ send_json_response(
1234
+ socket,
1235
+ "400 Bad Request",
1236
+ "{\"error\":\"invalid video generation request body\"}",
1237
+ )
1238
+ .await;
1239
+ }
1240
+ }
898
1241
  ("GET", "/api/image-gen/providers") => {
899
1242
  let config = load_config().unwrap_or_default();
900
1243
  let mut available: Vec<String> = Vec::new();
@@ -947,6 +1290,7 @@ async fn run_web_agent_loop(
947
1290
  &request.message,
948
1291
  &root,
949
1292
  request.image_data_uri.clone(),
1293
+ request.video_data_uri.clone(),
950
1294
  request.chat_id.as_deref(),
951
1295
  request.agent_id.as_deref(),
952
1296
  fast_mode,
@@ -1125,6 +1469,25 @@ fn picture_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
1125
1469
  Ok((picture.mime_type, bytes))
1126
1470
  }
1127
1471
 
1472
+ fn thumbnail_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
1473
+ if filename.contains('/') || filename.contains('\\') || filename.contains("..") {
1474
+ return Err("invalid thumbnail path".into());
1475
+ }
1476
+ let id = filename
1477
+ .strip_suffix(".thumb.png")
1478
+ .ok_or_else(|| "invalid thumbnail name".to_string())?;
1479
+ let picture = list_saved_pictures()
1480
+ .map_err(|error| error.to_string())?
1481
+ .into_iter()
1482
+ .find(|entry| entry.id == id)
1483
+ .ok_or_else(|| "picture not found".to_string())?;
1484
+ let thumb_path = picture
1485
+ .thumbnail_path
1486
+ .ok_or_else(|| "thumbnail not found".to_string())?;
1487
+ let bytes = std::fs::read(&thumb_path).map_err(|error| error.to_string())?;
1488
+ Ok(("image/png".to_string(), bytes))
1489
+ }
1490
+
1128
1491
  fn query_param(query: &str, key: &str) -> Option<String> {
1129
1492
  query.split('&').find_map(|pair| {
1130
1493
  let (name, value) = pair.split_once('=')?;