@pheem49/mint 1.7.1 → 1.8.1

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 (66) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +40 -0
  5. package/Release_Note.md +95 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/crates/mint-cli/Cargo.toml +1 -0
  8. package/crates/mint-cli/src/agent.rs +142 -48
  9. package/crates/mint-cli/src/main.rs +231 -22
  10. package/crates/mint-core/src/api_server.rs +71 -4
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/chat.rs +79 -0
  13. package/crates/mint-core/src/config.rs +69 -1
  14. package/crates/mint-core/src/lib.rs +6 -1
  15. package/crates/mint-core/src/orchestration.rs +137 -1
  16. package/crates/mint-core/src/skills.rs +113 -4
  17. package/crates/mint-core/src/workflows.rs +9 -22
  18. package/package.json +2 -1
  19. package/src/renderer/src/App.tsx +1 -1
  20. package/src/renderer/src/components/ChatPanel.tsx +197 -47
  21. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  22. package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
  23. package/src/renderer/src/components/MintDashboard.tsx +239 -7
  24. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  25. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  26. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  27. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  28. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  29. package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
  30. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  31. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  32. package/src/renderer/src/components/SettingsWindow.tsx +175 -970
  33. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  34. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  35. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  36. package/src/renderer/src/css/image-studio.css +15 -4
  37. package/src/renderer/src/css/settings.css +75 -6
  38. package/src/renderer/src/css/styles.css +419 -49
  39. package/src/renderer/src/css/workflows.css +542 -0
  40. package/src/renderer/src/env.d.ts +2 -0
  41. package/src/renderer/src/index.css +13 -1
  42. package/src/renderer/src/tauri.ts +140 -2
  43. package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
  44. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  45. package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
  46. package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
  47. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  48. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  49. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  50. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  51. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  52. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
  53. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  54. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  55. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
  56. package/src/renderer/src-web/css/image-studio.css +15 -4
  57. package/src/renderer/src-web/css/settings.css +75 -6
  58. package/src/renderer/src-web/css/styles.css +394 -50
  59. package/src/renderer/src-web/env.d.ts +2 -0
  60. package/src/renderer/src-web/index.css +12 -1
  61. package/src/renderer/src-web/tauri.ts +60 -2
  62. package/src-tauri/src/browser.rs +1 -141
  63. package/src-tauri/src/headless.rs +3 -2
  64. package/src-tauri/src/lib.rs +195 -111
  65. package/src-tauri/src/workflows.rs +56 -4
  66. package/src-tauri/tauri.conf.json +1 -1
@@ -110,6 +110,8 @@ enum Command {
110
110
  },
111
111
  /// Run one queued or supplied task through the native CLI agent.
112
112
  Agent { task: Option<String> },
113
+ /// Start the browser automation environment and enable browser actions.
114
+ Auto,
113
115
  /// Launch the web UI and local API server.
114
116
  Web,
115
117
  /// Start only the local API server.
@@ -453,6 +455,21 @@ enum MemoryCommand {
453
455
  },
454
456
  }
455
457
 
458
+ fn print_mcp_servers(servers: &std::collections::BTreeMap<String, mint_core::mcp::McpServer>) {
459
+ if servers.is_empty() {
460
+ println!("{DIM}(No MCP servers configured.){RESET}\n");
461
+ return;
462
+ }
463
+ for (name, srv) in servers {
464
+ let args_str = srv.args.join(" ");
465
+ println!(
466
+ " {BLUE}●{RESET} {name} {DIM}({} {}){RESET}",
467
+ srv.command, args_str
468
+ );
469
+ }
470
+ println!();
471
+ }
472
+
456
473
  #[tokio::main]
457
474
  async fn main() -> Result<()> {
458
475
  match Cli::parse().command {
@@ -523,6 +540,99 @@ async fn main() -> Result<()> {
523
540
  Command::Agent { task } => {
524
541
  run_cli_agent_task(task).await?;
525
542
  }
543
+ Command::Auto => {
544
+ println!("🚀 Starting Mint Browser Automation Environment...");
545
+ let config = mint_core::load_config()?;
546
+ mint_core::spawn_automation_browser(&config)
547
+ .await
548
+ .map_err(anyhow::Error::msg)?;
549
+
550
+ // Enable the browser tools if they are disabled
551
+ let mut config_mut = config.clone();
552
+ let mut changed = false;
553
+ for tool in &[
554
+ "browser_open",
555
+ "browser_click",
556
+ "browser_type",
557
+ "browser_read",
558
+ ] {
559
+ if config_mut.disabled_tools.contains(&tool.to_string()) {
560
+ config_mut.disabled_tools.retain(|x| x != *tool);
561
+ changed = true;
562
+ }
563
+ }
564
+ if changed {
565
+ mint_core::save_config(&config_mut)?;
566
+ println!(
567
+ "✅ Enabled browser automation tools in config: browser_open, browser_click, browser_type, browser_read"
568
+ );
569
+ }
570
+
571
+ println!(
572
+ "🌐 Isolated browser running with remote debugging on http://127.0.0.1:9222"
573
+ );
574
+ println!(
575
+ "💬 Keep this terminal open while you want Mint to automate browser tasks."
576
+ );
577
+ println!("Press Ctrl+C to terminate the automation browser session.");
578
+ println!("----------------------------------------------------------------------");
579
+
580
+ // Start tailing the log file in a background task
581
+ let log_dir = dirs::config_dir()
582
+ .unwrap_or_else(|| std::env::temp_dir())
583
+ .join("mint");
584
+ let log_file = log_dir.join("browser-automation.log");
585
+
586
+ // Clear existing log file on startup
587
+ let _ = std::fs::remove_file(&log_file);
588
+
589
+ let log_file_clone = log_file.clone();
590
+ tokio::spawn(async move {
591
+ use std::io::{BufRead, BufReader, Seek, SeekFrom};
592
+ let mut file_pos = 0;
593
+ loop {
594
+ if log_file_clone.exists() {
595
+ if let Ok(mut file) = std::fs::File::open(&log_file_clone) {
596
+ if let Ok(_) = file.seek(SeekFrom::Start(file_pos)) {
597
+ let reader = BufReader::new(file);
598
+ for line in reader.lines() {
599
+ if let Ok(line_str) = line {
600
+ if line_str.contains("[NAVIGATE]")
601
+ || line_str.contains("[NAVIGATE_SUCCESS]")
602
+ {
603
+ println!("🌐 {line_str}");
604
+ } else if line_str.contains("[CLICK]")
605
+ || line_str.contains("[CLICK_SUCCESS]")
606
+ {
607
+ println!("🖱️ {line_str}");
608
+ } else if line_str.contains("[TYPE]")
609
+ || line_str.contains("[TYPE_SUCCESS]")
610
+ {
611
+ println!("⌨️ {line_str}");
612
+ } else if line_str.contains("[READ]")
613
+ || line_str.contains("[READ_SUCCESS]")
614
+ {
615
+ println!("📖 {line_str}");
616
+ } else if line_str.contains("_ERROR]") {
617
+ println!("❌ {line_str}");
618
+ } else {
619
+ println!("📝 {line_str}");
620
+ }
621
+ }
622
+ }
623
+ if let Ok(pos) = log_file_clone.metadata().map(|m| m.len()) {
624
+ file_pos = pos;
625
+ }
626
+ }
627
+ }
628
+ }
629
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
630
+ }
631
+ });
632
+
633
+ tokio::signal::ctrl_c().await.ok();
634
+ println!("\n👋 Terminating Mint Browser Automation Environment...");
635
+ }
526
636
  Command::Web => {
527
637
  launch_mint_target("web".into()).await?;
528
638
  }
@@ -539,7 +649,10 @@ async fn main() -> Result<()> {
539
649
  mcp::add(&name, &command, args, env)?;
540
650
  println!("Added MCP server: {name}");
541
651
  }
542
- McpCommand::List => println!("{}", serde_json::to_string_pretty(&mcp::list()?)?),
652
+ McpCommand::List => {
653
+ println!("\n{BLUE}MCP servers:{RESET}");
654
+ print_mcp_servers(&mcp::list()?);
655
+ }
543
656
  McpCommand::Remove { name } => {
544
657
  println!(
545
658
  "{}",
@@ -585,10 +698,55 @@ async fn main() -> Result<()> {
585
698
  Command::Learn { path, list, delete } => {
586
699
  let memory = MemoryStore::open_default()?;
587
700
  if list {
588
- println!(
589
- "{}",
590
- serde_json::to_string_pretty(&memory.learned_skills(100)?)?
591
- );
701
+ let mut skills = memory.learned_skills(100)?;
702
+
703
+ if let Some(home) = dirs::home_dir() {
704
+ let global_skills_path =
705
+ home.join(".config").join("mint").join("mint-skills");
706
+ mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
707
+ }
708
+ if let Ok(root) = std::env::current_dir() {
709
+ if let Ok(root) = root.canonicalize() {
710
+ let workspace_skills_path1 = root.join(".agents").join("skills");
711
+ mint_core::skills::load_skills_from_dir(
712
+ &workspace_skills_path1,
713
+ &mut skills,
714
+ );
715
+
716
+ let workspace_skills_path2 = root.join("skills");
717
+ mint_core::skills::load_skills_from_dir(
718
+ &workspace_skills_path2,
719
+ &mut skills,
720
+ );
721
+ }
722
+ }
723
+
724
+ let mut unique_skills = std::collections::BTreeMap::new();
725
+ for skill in skills {
726
+ let loc = if skill.source_path.contains("/.config/mint/mint-skills") {
727
+ "Global"
728
+ } else if skill.source_path.contains("/skills")
729
+ || skill.source_path.contains("/.agents/skills")
730
+ {
731
+ "Workspace"
732
+ } else {
733
+ "Taught"
734
+ };
735
+ unique_skills.insert(skill.name.clone(), (skill, loc));
736
+ }
737
+
738
+ if unique_skills.is_empty() {
739
+ println!("No learned skills found.");
740
+ } else {
741
+ println!("Learned AI Skills:");
742
+ for (name, (skill, loc)) in &unique_skills {
743
+ if *loc == "Taught" {
744
+ println!(" ● [{}] {}", loc, name);
745
+ } else {
746
+ println!(" ● [{}] {} (Source: {})", loc, name, skill.source_path);
747
+ }
748
+ }
749
+ }
592
750
  } else if let Some(identifier) = delete {
593
751
  println!("{}", memory.delete_learned_skill(&identifier)?);
594
752
  } else if let Some(path) = path {
@@ -1315,6 +1473,9 @@ impl CliStreamFormatter {
1315
1473
  print_fn(ch);
1316
1474
  }
1317
1475
  print_fn('\n');
1476
+ for ch in " ".chars() {
1477
+ print_fn(ch);
1478
+ }
1318
1479
  } else {
1319
1480
  if is_table_line(&line) {
1320
1481
  self.table_buffer.push(line);
@@ -1327,6 +1488,9 @@ impl CliStreamFormatter {
1327
1488
  print_fn(ch);
1328
1489
  }
1329
1490
  print_fn('\n');
1491
+ for ch in " ".chars() {
1492
+ print_fn(ch);
1493
+ }
1330
1494
  }
1331
1495
  }
1332
1496
  }
@@ -1792,7 +1956,56 @@ async fn handle_slash_command(
1792
1956
 
1793
1957
  "/learn" => {
1794
1958
  if rest.is_empty() {
1795
- println!("{WARN}/learn usage: /learn <path>{RESET}\n");
1959
+ let mut skills = match MemoryStore::open_default() {
1960
+ Ok(m) => match m.learned_skills(100) {
1961
+ Ok(s) => s,
1962
+ Err(e) => {
1963
+ println!("{ERROR}Error loading custom skills: {e}{RESET}\n");
1964
+ return Some(SlashResult::Handled);
1965
+ }
1966
+ },
1967
+ Err(e) => {
1968
+ println!("{ERROR}Memory error: {e}{RESET}\n");
1969
+ return Some(SlashResult::Handled);
1970
+ }
1971
+ };
1972
+
1973
+ if let Some(home) = dirs::home_dir() {
1974
+ let global_skills_path = home.join(".config").join("mint").join("mint-skills");
1975
+ mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
1976
+ }
1977
+ let workspace_skills_path1 = session.current_dir.join(".agents").join("skills");
1978
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
1979
+ let workspace_skills_path2 = session.current_dir.join("skills");
1980
+ mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
1981
+
1982
+ let mut unique_skills = std::collections::BTreeMap::new();
1983
+ for skill in skills {
1984
+ let loc = if skill.source_path.contains("/.config/mint/mint-skills") {
1985
+ "Global"
1986
+ } else if skill.source_path.contains("/skills")
1987
+ || skill.source_path.contains("/.agents/skills")
1988
+ {
1989
+ "Workspace"
1990
+ } else {
1991
+ "Taught"
1992
+ };
1993
+ unique_skills.insert(skill.name.clone(), (skill, loc));
1994
+ }
1995
+
1996
+ if unique_skills.is_empty() {
1997
+ println!("No learned skills found. Use '/learn <path>' to add one.\n");
1998
+ } else {
1999
+ println!("Learned AI Skills:");
2000
+ for (name, (skill, loc)) in &unique_skills {
2001
+ if *loc == "Taught" {
2002
+ println!(" ● [{}] {}", loc, name);
2003
+ } else {
2004
+ println!(" ● [{}] {} (Source: {})", loc, name, skill.source_path);
2005
+ }
2006
+ }
2007
+ println!();
2008
+ }
1796
2009
  } else {
1797
2010
  let path = PathBuf::from(rest);
1798
2011
  let path = if path.is_absolute() {
@@ -1834,8 +2047,10 @@ async fn handle_slash_command(
1834
2047
  println!(
1835
2048
  " {DIM}[{}]{RESET} {BLUE}You:{RESET} {}",
1836
2049
  &item.created_at[..16.min(item.created_at.len())],
1837
- if item.user_text.len() > 80 {
1838
- format!("{}…", &item.user_text[..80])
2050
+ if item.user_text.chars().count() > 80 {
2051
+ let short: String =
2052
+ item.user_text.chars().take(80).collect();
2053
+ format!("{}…", short)
1839
2054
  } else {
1840
2055
  item.user_text.clone()
1841
2056
  }
@@ -1903,15 +2118,9 @@ async fn handle_slash_command(
1903
2118
  .unwrap_or((rest, ""));
1904
2119
  match subcmd {
1905
2120
  "list" | "" => match mcp::list() {
1906
- Ok(servers) if servers.is_empty() => {
1907
- println!("{DIM}No MCP servers configured.{RESET}\n");
1908
- }
1909
2121
  Ok(servers) => {
1910
2122
  println!("\n{BLUE}MCP servers:{RESET}");
1911
- match serde_json::to_string_pretty(&servers) {
1912
- Ok(json) => println!("{json}\n"),
1913
- Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
1914
- }
2123
+ print_mcp_servers(&servers);
1915
2124
  }
1916
2125
  Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
1917
2126
  },
@@ -2291,7 +2500,7 @@ async fn run_interactive_chat() -> Result<()> {
2291
2500
  .to_owned();
2292
2501
 
2293
2502
  println!();
2294
- print!("{MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
2503
+ print!(" {MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
2295
2504
  let _ = io::stdout().flush();
2296
2505
 
2297
2506
  let mut system_instruction = format!(
@@ -2337,7 +2546,7 @@ async fn run_interactive_chat() -> Result<()> {
2337
2546
  |chunk| {
2338
2547
  if first_chunk {
2339
2548
  first_chunk = false;
2340
- print!("\r\x1b[2K{MINT}Mint:{RESET} ");
2549
+ print!("\r\x1b[2K {MINT}Mint:{RESET} ");
2341
2550
  }
2342
2551
  filter.process_chunk(&chunk, |text| {
2343
2552
  for c in text.chars() {
@@ -2926,7 +3135,7 @@ fn read_line_interactive(
2926
3135
  expanded_str = expanded_str.replace(placeholder_str, content);
2927
3136
  }
2928
3137
 
2929
- println!("{BLUE}You ›{RESET} {}", expanded_str);
3138
+ println!(" {BLUE}You ›{RESET} {}", expanded_str);
2930
3139
  let _ = io::stdout().flush();
2931
3140
 
2932
3141
  break Some(InteractiveInput {
@@ -3157,7 +3366,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3157
3366
  .to_string();
3158
3367
 
3159
3368
  if SESSION_APPROVED.load(std::sync::atomic::Ordering::Relaxed) {
3160
- println!("{} {MINT}Approve (session-wide){RESET}", clean_prompt);
3369
+ println!(" {} {MINT}Approve (session-wide){RESET}", clean_prompt);
3161
3370
  return Ok(true);
3162
3371
  }
3163
3372
 
@@ -3166,7 +3375,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3166
3375
  use crossterm::tty::IsTty;
3167
3376
 
3168
3377
  if !io::stdout().is_tty() || enable_raw_mode().is_err() {
3169
- print!("{} [y/N] ", clean_prompt);
3378
+ print!(" {} [y/N] ", clean_prompt);
3170
3379
  let _ = io::stdout().flush();
3171
3380
  let mut answer = String::new();
3172
3381
  io::stdin().read_line(&mut answer)?;
@@ -3177,7 +3386,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3177
3386
  }
3178
3387
 
3179
3388
  let _ = disable_raw_mode();
3180
- println!("{}", clean_prompt);
3389
+ println!(" {}", clean_prompt);
3181
3390
 
3182
3391
  let options = ["Approve", "Approve this session", "No"];
3183
3392
  let mut selected = 0;
@@ -3284,7 +3493,7 @@ pub fn confirm(prompt: &str) -> Result<bool> {
3284
3493
  1 => format!("{MINT}Approve this session{RESET}"),
3285
3494
  _ => format!("{ERROR}No{RESET}"),
3286
3495
  };
3287
- println!("{} {}", clean_prompt, result_str);
3496
+ println!(" {} {}", clean_prompt, result_str);
3288
3497
  let _ = io::stdout().flush();
3289
3498
 
3290
3499
  match choice {
@@ -147,6 +147,15 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
147
147
  });
148
148
  send_json_response(socket, "200 OK", &status_json.to_string()).await;
149
149
  }
150
+ ("GET", "/api/detect-tools") => {
151
+ let tools_json = serde_json::json!({
152
+ "docker": crate::config::which("docker"),
153
+ "git": crate::config::which("git"),
154
+ "gh": crate::config::which("gh"),
155
+ "node": crate::config::which("node")
156
+ });
157
+ send_json_response(socket, "200 OK", &tools_json.to_string()).await;
158
+ }
150
159
  ("GET", "/api/system-info") => {
151
160
  send_json_response(socket, "200 OK", &system_info().to_string()).await;
152
161
  }
@@ -272,6 +281,58 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
272
281
  .await;
273
282
  }
274
283
  }
284
+ ("POST", "/api/interactions") => {
285
+ #[derive(Deserialize)]
286
+ #[serde(rename_all = "camelCase")]
287
+ struct ApiSaveInteraction {
288
+ chat_id: String,
289
+ user_text: String,
290
+ provider: String,
291
+ model: String,
292
+ }
293
+
294
+ if let Ok(req) = serde_json::from_str::<ApiSaveInteraction>(body) {
295
+ if let Ok(memory) = MemoryStore::open_default() {
296
+ match memory.add_interaction_for_chat(
297
+ &req.chat_id,
298
+ &req.user_text,
299
+ "",
300
+ &req.provider,
301
+ &req.model,
302
+ ) {
303
+ Ok(row_id) => {
304
+ let res_json = json!({ "success": true, "id": row_id });
305
+ send_json_response(socket, "200 OK", &res_json.to_string())
306
+ .await;
307
+ }
308
+ Err(error) => {
309
+ let err_json =
310
+ json!({ "success": false, "message": error.to_string() });
311
+ send_json_response(
312
+ socket,
313
+ "500 Internal Server Error",
314
+ &err_json.to_string(),
315
+ )
316
+ .await;
317
+ }
318
+ }
319
+ } else {
320
+ send_json_response(
321
+ socket,
322
+ "500 Internal Server Error",
323
+ "{\"success\":false,\"message\":\"db error\"}",
324
+ )
325
+ .await;
326
+ }
327
+ } else {
328
+ send_json_response(
329
+ socket,
330
+ "400 Bad Request",
331
+ "{\"success\":false,\"message\":\"invalid body\"}",
332
+ )
333
+ .await;
334
+ }
335
+ }
275
336
  ("POST", "/api/interactions/agent-activity") => {
276
337
  #[derive(Deserialize)]
277
338
  #[serde(rename_all = "camelCase")]
@@ -414,19 +475,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
414
475
  chat_id: Option<String>,
415
476
  image_data_uri: Option<String>,
416
477
  audio_data_uri: Option<String>,
478
+ document_attachment: Option<crate::chat::DocumentAttachment>,
417
479
  }
418
480
 
419
481
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
420
482
  let config = load_config().unwrap_or_default();
421
- let mut chat_req = ChatRequest {
483
+ let chat_req = ChatRequest {
422
484
  message: req.message,
423
485
  system_instruction: req.system_instruction.unwrap_or_default(),
424
486
  chat_id: req.chat_id,
425
487
  image_data_uri: req.image_data_uri,
426
488
  audio_data_uri: req.audio_data_uri,
427
- document_attachment: None,
489
+ document_attachment: req.document_attachment,
428
490
  workspace_path: None,
429
491
  };
492
+ let mut chat_req =
493
+ chat_req.with_document_context(&config).unwrap_or(chat_req);
430
494
  let sent_image = chat_req.image_data_uri.clone();
431
495
  let sent_message = chat_req.message.clone();
432
496
 
@@ -495,19 +559,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
495
559
  chat_id: Option<String>,
496
560
  image_data_uri: Option<String>,
497
561
  audio_data_uri: Option<String>,
562
+ document_attachment: Option<crate::chat::DocumentAttachment>,
498
563
  }
499
564
 
500
565
  if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
501
566
  let config = load_config().unwrap_or_default();
502
- let mut chat_req = ChatRequest {
567
+ let chat_req = ChatRequest {
503
568
  message: req.message,
504
569
  system_instruction: req.system_instruction.unwrap_or_default(),
505
570
  chat_id: req.chat_id,
506
571
  image_data_uri: req.image_data_uri,
507
572
  audio_data_uri: req.audio_data_uri,
508
- document_attachment: None,
573
+ document_attachment: req.document_attachment,
509
574
  workspace_path: None,
510
575
  };
576
+ let mut chat_req =
577
+ chat_req.with_document_context(&config).unwrap_or(chat_req);
511
578
  let sent_image = chat_req.image_data_uri.clone();
512
579
  let sent_message = chat_req.message.clone();
513
580