@pheem49/mint 1.8.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/Cargo.lock +55 -3
  2. package/Cargo.toml +2 -1
  3. package/Release_Note.md +297 -107
  4. package/bin/mint +0 -0
  5. package/crates/mint-cli/Cargo.toml +1 -0
  6. package/crates/mint-cli/src/actions.rs +233 -0
  7. package/crates/mint-cli/src/agent.rs +382 -127
  8. package/crates/mint-cli/src/gmail.rs +22 -2
  9. package/crates/mint-cli/src/image.rs +8 -8
  10. package/crates/mint-cli/src/interactive.rs +2888 -0
  11. package/crates/mint-cli/src/main.rs +391 -2506
  12. package/crates/mint-cli/src/markdown.rs +294 -0
  13. package/crates/mint-cli/src/onboard.rs +152 -109
  14. package/crates/mint-cli/src/setup.rs +143 -143
  15. package/crates/mint-cli/src/updater.rs +48 -6
  16. package/crates/mint-core/Cargo.toml +1 -0
  17. package/crates/mint-core/src/agent_loop.rs +1 -0
  18. package/crates/mint-core/src/api_server.rs +431 -65
  19. package/crates/mint-core/src/browser.rs +630 -102
  20. package/crates/mint-core/src/channels.rs +105 -6
  21. package/crates/mint-core/src/chat.rs +174 -11
  22. package/crates/mint-core/src/code_tools.rs +47 -31
  23. package/crates/mint-core/src/config.rs +302 -17
  24. package/crates/mint-core/src/image_gen.rs +299 -38
  25. package/crates/mint-core/src/lib.rs +10 -7
  26. package/crates/mint-core/src/mcp.rs +119 -6
  27. package/crates/mint-core/src/memory.rs +6 -1
  28. package/crates/mint-core/src/orchestration.rs +237 -96
  29. package/crates/mint-core/src/pictures.rs +67 -5
  30. package/crates/mint-core/src/semantic.rs +1 -1
  31. package/crates/mint-core/src/shell.rs +1 -1
  32. package/crates/mint-core/src/skills.rs +128 -25
  33. package/crates/mint-core/src/video_gen.rs +235 -0
  34. package/package.json +8 -2
  35. package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
  36. package/shared-modules/linux-audio/fluidsynth2.json +8 -2
  37. package/shared-modules/linux-audio/lilv.json +26 -19
  38. package/shared-modules/linux-audio/lv2.json +0 -1
  39. package/shared-modules/luajit/luajit.json +2 -2
  40. package/skills-lock.json +41 -0
  41. package/src/renderer/index-web.html +1 -1
  42. package/src/renderer/index.html +1 -1
  43. package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
  44. package/src/renderer/shared/constants/slashCommands.ts +27 -0
  45. package/src/renderer/shared/platform.ts +3 -0
  46. package/src/renderer/shared/types.ts +4 -0
  47. package/src/renderer/shared/utils/approval.tsx +86 -7
  48. package/src/renderer/shared/utils/fileIcons.ts +135 -0
  49. package/src/renderer/shared/utils/markdown.tsx +217 -88
  50. package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
  51. package/src/renderer/src/components/ChatPanel.tsx +486 -69
  52. package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
  53. package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
  54. package/src/renderer/src/components/MintDashboard.tsx +195 -7
  55. package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
  56. package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
  57. package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
  58. package/src/renderer/src/components/Settings/GeneralTab.tsx +795 -413
  59. package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
  60. package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
  61. package/src/renderer/src/components/SettingsWindow.tsx +106 -3
  62. package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
  63. package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
  64. package/src/renderer/src/css/image-studio.css +106 -0
  65. package/src/renderer/src/css/settings.css +158 -0
  66. package/src/renderer/src/css/styles.css +368 -1
  67. package/src/renderer/src/css/veo-studio.css +626 -0
  68. package/src/renderer/src/index.css +1 -0
  69. package/src/renderer/src/tauri.ts +144 -17
  70. package/src/renderer/src-web/components/ChatPanel.tsx +385 -93
  71. package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
  72. package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
  73. package/src/renderer/src-web/components/MintDashboard.tsx +156 -5
  74. package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
  75. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
  76. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
  77. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +795 -411
  78. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
  79. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
  80. package/src/renderer/src-web/components/SettingsWindow.tsx +110 -3
  81. package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
  82. package/src/renderer/src-web/css/image-studio.css +106 -0
  83. package/src/renderer/src-web/css/settings.css +158 -0
  84. package/src/renderer/src-web/css/styles.css +369 -1
  85. package/src/renderer/src-web/css/veo-studio.css +650 -0
  86. package/src/renderer/src-web/index.css +1 -0
  87. package/src/renderer/src-web/tauri.ts +135 -16
  88. package/src-tauri/src/desktop.rs +18 -16
  89. package/src-tauri/src/discord_rpc.rs +1 -1
  90. package/src-tauri/src/lib.rs +155 -25
  91. package/src-tauri/src/webhooks.rs +45 -31
  92. package/src-tauri/src/workflows.rs +1 -0
  93. package/src-tauri/tauri.conf.json +1 -1
  94. package/temp_gridgeist/AGENTS.md +8 -0
  95. package/temp_gridgeist/CHANGELOG.md +130 -0
  96. package/temp_gridgeist/LICENSE +21 -0
  97. package/temp_gridgeist/README.md +323 -0
  98. package/temp_gridgeist/README.th.md +325 -0
  99. package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
  100. package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
  101. package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
  102. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
  103. package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
  104. package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
  105. package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
  106. package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
@@ -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();
@@ -213,16 +252,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
213
252
  new_title: String,
214
253
  }
215
254
 
216
- if let Ok(req) = serde_json::from_str::<RenameRequest>(body) {
217
- if let Ok(memory) = MemoryStore::open_default() {
218
- let updated = memory
219
- .rename_chat_session(&req.chat_id, &req.new_title)
220
- .unwrap_or(0);
221
- let response =
222
- serde_json::json!({ "status": "ok", "updated": updated });
223
- send_json_response(socket, "200 OK", &response.to_string()).await;
224
- return;
225
- }
255
+ if let Ok(req) = serde_json::from_str::<RenameRequest>(body)
256
+ && let Ok(memory) = MemoryStore::open_default()
257
+ {
258
+ let updated = memory
259
+ .rename_chat_session(&req.chat_id, &req.new_title)
260
+ .unwrap_or(0);
261
+ let response = serde_json::json!({ "status": "ok", "updated": updated });
262
+ send_json_response(socket, "200 OK", &response.to_string()).await;
263
+ return;
226
264
  }
227
265
  send_json_response(
228
266
  socket,
@@ -231,6 +269,48 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
231
269
  )
232
270
  .await;
233
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
+ }
234
314
  ("GET", "/api/profile") => {
235
315
  let key = query_param(query, "key").unwrap_or_default();
236
316
  if let Ok(memory) = MemoryStore::open_default() {
@@ -252,10 +332,39 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
252
332
  key: String,
253
333
  value: String,
254
334
  }
255
- if let Ok(req) = serde_json::from_str::<ProfileRequest>(body) {
256
- if let Ok(memory) = MemoryStore::open_default() {
257
- let _ = memory.set_profile(&req.key, &req.value);
258
- send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
335
+ if let Ok(req) = serde_json::from_str::<ProfileRequest>(body)
336
+ && let Ok(memory) = MemoryStore::open_default()
337
+ {
338
+ let _ = memory.set_profile(&req.key, &req.value);
339
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
340
+ return;
341
+ }
342
+ send_json_response(
343
+ socket,
344
+ "500 Internal Server Error",
345
+ "{\"status\":\"error\"}",
346
+ )
347
+ .await;
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;
259
368
  return;
260
369
  }
261
370
  }
@@ -287,6 +396,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
287
396
  struct ApiSaveInteraction {
288
397
  chat_id: String,
289
398
  user_text: String,
399
+ ai_text: Option<String>,
290
400
  provider: String,
291
401
  model: String,
292
402
  }
@@ -296,7 +406,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
296
406
  match memory.add_interaction_for_chat(
297
407
  &req.chat_id,
298
408
  &req.user_text,
299
- "",
409
+ req.ai_text.as_deref().unwrap_or(""),
300
410
  &req.provider,
301
411
  &req.model,
302
412
  ) {
@@ -341,25 +451,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
341
451
  activity: Vec<AgentProgress>,
342
452
  }
343
453
 
344
- if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body) {
345
- if let Ok(memory) = MemoryStore::open_default() {
346
- match serde_json::to_string(&payload.activity) {
347
- Ok(activity_json) => {
348
- if memory
349
- .set_interaction_agent_activity_json(
350
- payload.interaction_id,
351
- &activity_json,
352
- )
353
- .is_ok()
354
- {
355
- send_json_response(socket, "200 OK", "{\"status\":\"ok\"}")
356
- .await;
357
- return;
358
- }
359
- }
360
- Err(_) => {}
361
- }
362
- }
454
+ if let Ok(payload) = serde_json::from_str::<SaveAgentActivityRequest>(body)
455
+ && let Ok(memory) = MemoryStore::open_default()
456
+ && let Ok(activity_json) = serde_json::to_string(&payload.activity)
457
+ && memory
458
+ .set_interaction_agent_activity_json(
459
+ payload.interaction_id,
460
+ &activity_json,
461
+ )
462
+ .is_ok()
463
+ {
464
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
465
+ return;
363
466
  }
364
467
  send_json_response(
365
468
  socket,
@@ -397,6 +500,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
397
500
  }
398
501
  }
399
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
+ }
400
519
  ("GET", "/api/config") => {
401
520
  let config = load_config().unwrap_or_default();
402
521
  if let Ok(json_str) = serde_json::to_string(&config) {
@@ -406,11 +525,11 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
406
525
  }
407
526
  }
408
527
  ("POST", "/api/config") => {
409
- if let Ok(new_config) = serde_json::from_str::<MintConfig>(body) {
410
- if save_config(&new_config).is_ok() {
411
- send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
412
- return;
413
- }
528
+ if let Ok(new_config) = serde_json::from_str::<MintConfig>(body)
529
+ && save_config(&new_config).is_ok()
530
+ {
531
+ send_json_response(socket, "200 OK", "{\"status\":\"ok\"}").await;
532
+ return;
414
533
  }
415
534
  send_json_response(
416
535
  socket,
@@ -466,6 +585,71 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
466
585
  .await;
467
586
  }
468
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
+
469
653
  ("POST", "/api/chat") => {
470
654
  #[derive(Deserialize)]
471
655
  #[serde(rename_all = "camelCase")]
@@ -475,6 +659,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
475
659
  chat_id: Option<String>,
476
660
  image_data_uri: Option<String>,
477
661
  audio_data_uri: Option<String>,
662
+ video_data_uri: Option<String>,
478
663
  document_attachment: Option<crate::chat::DocumentAttachment>,
479
664
  agent_id: Option<String>,
480
665
  }
@@ -487,6 +672,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
487
672
  chat_id: req.chat_id,
488
673
  image_data_uri: req.image_data_uri,
489
674
  audio_data_uri: req.audio_data_uri,
675
+ video_data_uri: req.video_data_uri,
490
676
  document_attachment: req.document_attachment,
491
677
  workspace_path: None,
492
678
  agent_id: req.agent_id,
@@ -494,6 +680,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
494
680
  let mut chat_req =
495
681
  chat_req.with_document_context(&config).unwrap_or(chat_req);
496
682
  let sent_image = chat_req.image_data_uri.clone();
683
+ let sent_video = chat_req.video_data_uri.clone();
497
684
  let sent_message = chat_req.message.clone();
498
685
 
499
686
  let response = if let Some(clean_message) =
@@ -513,6 +700,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
513
700
 
514
701
  match response {
515
702
  Ok(resp) => {
703
+ log_api_req(
704
+ "POST",
705
+ "/api/chat",
706
+ "200 OK",
707
+ Some(&format!("Model: {}", config.ai_provider)),
708
+ );
516
709
  if let Some(image) = sent_image {
517
710
  let _ = save_chat_images(
518
711
  image
@@ -520,6 +713,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
520
713
  .map(str::to_owned)
521
714
  .collect::<Vec<_>>(),
522
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()),
523
726
  Some(sent_message),
524
727
  );
525
728
  }
@@ -529,7 +732,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
529
732
  }
530
733
  }
531
734
  Err(e) => {
532
- eprintln!("API Chat error: {:?}", e);
735
+ log_api_err("API /api/chat error", &e);
533
736
  let err_json = serde_json::json!({
534
737
  "provider": "error",
535
738
  "model": "error",
@@ -579,6 +782,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
579
782
  chat_id: Option<String>,
580
783
  image_data_uri: Option<String>,
581
784
  audio_data_uri: Option<String>,
785
+ video_data_uri: Option<String>,
582
786
  document_attachment: Option<crate::chat::DocumentAttachment>,
583
787
  agent_id: Option<String>,
584
788
  }
@@ -591,6 +795,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
591
795
  chat_id: req.chat_id,
592
796
  image_data_uri: req.image_data_uri,
593
797
  audio_data_uri: req.audio_data_uri,
798
+ video_data_uri: req.video_data_uri,
594
799
  document_attachment: req.document_attachment,
595
800
  workspace_path: None,
596
801
  agent_id: req.agent_id,
@@ -598,6 +803,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
598
803
  let mut chat_req =
599
804
  chat_req.with_document_context(&config).unwrap_or(chat_req);
600
805
  let sent_image = chat_req.image_data_uri.clone();
806
+ let sent_video = chat_req.video_data_uri.clone();
601
807
  let sent_message = chat_req.message.clone();
602
808
 
603
809
  let is_chat = if let Some(clean_message) =
@@ -674,6 +880,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
674
880
 
675
881
  match result {
676
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
+ );
677
892
  if let Ok(json_val) =
678
893
  serde_json::to_string(&serde_json::json!({
679
894
  "type": "done",
@@ -684,7 +899,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
684
899
  }
685
900
  }
686
901
  Err(e) => {
687
- eprintln!("API Chat Stream error: {:?}", e);
902
+ log_api_err("API /api/chat-stream error", &e);
688
903
  let err_json = serde_json::json!({
689
904
  "type": "done",
690
905
  "response": {
@@ -693,22 +908,27 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
693
908
  "text": format!("Error orchestrating chat: {e}")
694
909
  }
695
910
  });
696
- let _ = tx_done
697
- .send(format!("{}\n", err_json.to_string()));
911
+ let _ = tx_done.send(format!("{}\n", err_json));
698
912
  }
699
913
  }
700
914
  });
701
915
 
702
916
  let abort_handle = join_handle.abort_handle();
703
917
  if !chat_id_str.is_empty() {
704
- crate::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
918
+ crate::ACTIVE_AGENTS
919
+ .lock()
920
+ .unwrap()
921
+ .insert(chat_id_str.clone(), abort_handle);
705
922
  }
706
923
 
707
924
  let chat_id_str_cleanup = chat_id_str.clone();
708
925
  tokio::spawn(async move {
709
926
  let _ = join_handle.await;
710
927
  if !chat_id_str_cleanup.is_empty() {
711
- crate::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str_cleanup);
928
+ crate::ACTIVE_AGENTS
929
+ .lock()
930
+ .unwrap()
931
+ .remove(&chat_id_str_cleanup);
712
932
  }
713
933
  });
714
934
  } else {
@@ -725,6 +945,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
725
945
  let chat_id_str = chat_id.clone().unwrap_or_default();
726
946
  let message = chat_req.message.clone();
727
947
  let image_data_uri = chat_req.image_data_uri.clone();
948
+ let video_data_uri = chat_req.video_data_uri.clone();
728
949
  let agent_id = chat_req.agent_id.clone();
729
950
 
730
951
  let join_handle = tokio::spawn(async move {
@@ -733,6 +954,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
733
954
  &message,
734
955
  &root,
735
956
  image_data_uri,
957
+ video_data_uri,
736
958
  chat_id.as_deref(),
737
959
  agent_id.as_deref(),
738
960
  fast_mode,
@@ -768,22 +990,27 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
768
990
  "text": format!("Error orchestrating agent: {e}")
769
991
  }
770
992
  });
771
- let _ = tx_done
772
- .send(format!("{}\n", err_json.to_string()));
993
+ let _ = tx_done.send(format!("{}\n", err_json));
773
994
  }
774
995
  }
775
996
  });
776
997
 
777
998
  let abort_handle = join_handle.abort_handle();
778
999
  if !chat_id_str.is_empty() {
779
- crate::ACTIVE_AGENTS.lock().unwrap().insert(chat_id_str.clone(), abort_handle);
1000
+ crate::ACTIVE_AGENTS
1001
+ .lock()
1002
+ .unwrap()
1003
+ .insert(chat_id_str.clone(), abort_handle);
780
1004
  }
781
1005
 
782
1006
  let chat_id_str_cleanup = chat_id_str.clone();
783
1007
  tokio::spawn(async move {
784
1008
  let _ = join_handle.await;
785
1009
  if !chat_id_str_cleanup.is_empty() {
786
- crate::ACTIVE_AGENTS.lock().unwrap().remove(&chat_id_str_cleanup);
1010
+ crate::ACTIVE_AGENTS
1011
+ .lock()
1012
+ .unwrap()
1013
+ .remove(&chat_id_str_cleanup);
787
1014
  }
788
1015
  });
789
1016
  }
@@ -805,6 +1032,16 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
805
1032
  .map(str::to_owned)
806
1033
  .collect::<Vec<_>>(),
807
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()),
808
1045
  Some(sent_message),
809
1046
  );
810
1047
  }
@@ -834,6 +1071,12 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
834
1071
  /// Which image provider to use (overrides config.image_gen_provider).
835
1072
  #[serde(default)]
836
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>,
837
1080
  }
838
1081
 
839
1082
  if let Ok(req) = serde_json::from_str::<ImageGenApiRequest>(body) {
@@ -845,9 +1088,18 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
845
1088
  num_images: req.num_images,
846
1089
  model: req.model,
847
1090
  provider: req.provider,
1091
+ image_data_uri: req.image_data_uri,
1092
+ mask_data_uri: req.mask_data_uri,
1093
+ mode: req.mode,
848
1094
  };
849
1095
  match generate_images(&config, &gen_request).await {
850
1096
  Ok(result) => {
1097
+ log_api_req(
1098
+ "POST",
1099
+ "/api/image-generate",
1100
+ "200 OK",
1101
+ Some(&format!("Provider: {}", result.provider)),
1102
+ );
851
1103
  let data_uris: Vec<String> = result
852
1104
  .images
853
1105
  .iter()
@@ -875,6 +1127,7 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
875
1127
  send_json_response(socket, "200 OK", &response.to_string()).await;
876
1128
  }
877
1129
  Err(e) => {
1130
+ log_api_err("API /api/image-generate error", &e);
878
1131
  let err = json!({ "error": e.to_string() });
879
1132
  send_json_response(
880
1133
  socket,
@@ -893,6 +1146,98 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
893
1146
  .await;
894
1147
  }
895
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
+ }
896
1241
  ("GET", "/api/image-gen/providers") => {
897
1242
  let config = load_config().unwrap_or_default();
898
1243
  let mut available: Vec<String> = Vec::new();
@@ -945,6 +1290,7 @@ async fn run_web_agent_loop(
945
1290
  &request.message,
946
1291
  &root,
947
1292
  request.image_data_uri.clone(),
1293
+ request.video_data_uri.clone(),
948
1294
  request.chat_id.as_deref(),
949
1295
  request.agent_id.as_deref(),
950
1296
  fast_mode,
@@ -1123,6 +1469,25 @@ fn picture_bytes(filename: &str) -> Result<(String, Vec<u8>), String> {
1123
1469
  Ok((picture.mime_type, bytes))
1124
1470
  }
1125
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
+
1126
1491
  fn query_param(query: &str, key: &str) -> Option<String> {
1127
1492
  query.split('&').find_map(|pair| {
1128
1493
  let (name, value) = pair.split_once('=')?;
@@ -1135,12 +1500,13 @@ fn percent_decode(raw: &str) -> String {
1135
1500
  let mut output = Vec::with_capacity(bytes.len());
1136
1501
  let mut index = 0;
1137
1502
  while index < bytes.len() {
1138
- if bytes[index] == b'%' && index + 2 < bytes.len() {
1139
- if let Ok(hex) = u8::from_str_radix(&raw[index + 1..index + 3], 16) {
1140
- output.push(hex);
1141
- index += 3;
1142
- continue;
1143
- }
1503
+ if bytes[index] == b'%'
1504
+ && index + 2 < bytes.len()
1505
+ && let Ok(hex) = u8::from_str_radix(&raw[index + 1..index + 3], 16)
1506
+ {
1507
+ output.push(hex);
1508
+ index += 3;
1509
+ continue;
1144
1510
  }
1145
1511
  output.push(if bytes[index] == b'+' {
1146
1512
  b' '