@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
@@ -302,6 +302,7 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
302
302
  "note_write",
303
303
  "run_plugin",
304
304
  "mcp_tool",
305
+ "mcp_list_tools",
305
306
  "run_shell",
306
307
  "verify",
307
308
  "apply_patch",
@@ -313,6 +314,10 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
313
314
  allowed_actions.push("browser_click");
314
315
  allowed_actions.push("browser_type");
315
316
  allowed_actions.push("browser_read");
317
+ allowed_actions.push("browser_mouse_move");
318
+ allowed_actions.push("browser_mouse_click");
319
+ allowed_actions.push("browser_key_press");
320
+ allowed_actions.push("browser_screenshot");
316
321
  }
317
322
 
318
323
  allowed_actions.retain(|action| !config.disabled_tools.contains(&action.to_string()));
@@ -354,15 +359,27 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
354
359
  input_formats.push("- browser_open: {\"url\":\"https://example.com\"}");
355
360
  }
356
361
  if allowed_actions.contains(&"browser_click") {
357
- input_formats.push("- browser_click: {\"selector\":\"button.submit-btn\"}");
362
+ input_formats.push("- browser_click: {\"selector\":\"button.submit-btn\"} (CSS selector, or text=Login, contains=Submit, xpath=//button)");
358
363
  }
359
364
  if allowed_actions.contains(&"browser_type") {
360
365
  input_formats
361
- .push("- browser_type: {\"selector\":\"input.search-bar\", \"text\":\"search query\"}");
366
+ .push("- browser_type: {\"selector\":\"input.search-bar\", \"text\":\"search query\"} (CSS selector, or text=Placeholder, contains=Label)");
362
367
  }
363
368
  if allowed_actions.contains(&"browser_read") {
364
369
  input_formats.push("- browser_read: {}");
365
370
  }
371
+ if allowed_actions.contains(&"browser_mouse_move") {
372
+ input_formats.push("- browser_mouse_move: {\"x\":640,\"y\":360}");
373
+ }
374
+ if allowed_actions.contains(&"browser_mouse_click") {
375
+ input_formats.push("- browser_mouse_click: {\"x\":640,\"y\":360,\"button\":\"left\"}");
376
+ }
377
+ if allowed_actions.contains(&"browser_key_press") {
378
+ input_formats.push("- browser_key_press: {\"key\":\"Enter\"} (keys: Enter,Tab,Escape,Backspace,Delete,ArrowUp,ArrowDown,ArrowLeft,ArrowRight,F1-F12,Space)");
379
+ }
380
+ if allowed_actions.contains(&"browser_screenshot") {
381
+ input_formats.push("- browser_screenshot: {}");
382
+ }
366
383
  if allowed_actions.contains(&"memory_recall") {
367
384
  input_formats.push("- memory_recall: {\"query\":\"what did user say about X\"}");
368
385
  }
@@ -410,8 +427,19 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
410
427
  if allowed_actions.contains(&"run_plugin") {
411
428
  input_formats.push("- run_plugin: {\"name\":\"gmail|google_calendar|notion|docker|spotify|obsidian|system_metrics\",\"instruction\":\"instruction string\"}");
412
429
  }
430
+ let mcp_str;
413
431
  if allowed_actions.contains(&"mcp_tool") {
414
- input_formats.push("- mcp_tool: {\"server\":\"configured-server\",\"tool\":\"tool-name\",\"arguments\":{}}");
432
+ let servers = crate::mcp::list_mcp_servers()
433
+ .map(|m| m.keys().cloned().collect::<Vec<String>>().join(", "))
434
+ .unwrap_or_default();
435
+ mcp_str = format!(
436
+ "- mcp_tool: {{\"server\":\"<name>\",\"tool\":\"tool-name\",\"arguments\":{{}}}} (Available configured servers: {})",
437
+ servers
438
+ );
439
+ input_formats.push(&mcp_str);
440
+ }
441
+ if allowed_actions.contains(&"mcp_list_tools") {
442
+ input_formats.push("- mcp_list_tools: {\"server\":\"configured-server\"}");
415
443
  }
416
444
  if allowed_actions.contains(&"run_shell") {
417
445
  input_formats.push(
@@ -447,7 +475,7 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
447
475
  );
448
476
  }
449
477
  if allowed_actions.contains(&"apply_patch") && allowed_actions.contains(&"write_file") {
450
- rules.push("3. Use apply_patch for all edits to existing files. write_file is only for creating new files inside the workspace.");
478
+ rules.push("3. Use apply_patch for all edits to existing files (write_file is only for creating new files). For edits across multiple sections of a file, supply separate small hunks in the 'hunks' array (one hunk per edit location). Keep oldText in each hunk minimal (only 1-3 exact lines needed to pinpoint the edit). NEVER bundle multiple separate edits into a single giant hunk that spans the entire file.");
451
479
  }
452
480
  if allowed_actions.contains(&"run_shell")
453
481
  || allowed_actions.contains(&"write_file")
@@ -468,16 +496,28 @@ pub fn build_system_prompt(config: &MintConfig) -> String {
468
496
  rules.push("7a. Use browser_open to navigate the virtual browser to a URL.");
469
497
  }
470
498
  if allowed_actions.contains(&"browser_click") {
471
- rules.push("7b. Use browser_click to click elements on the page.");
499
+ rules.push("7b. Use browser_click to click elements. Selector can be: CSS selector (button.class, #id, [attr=val]), text=ExactText to find by visible text, contains=PartialText for partial match, or xpath=//expr for XPath. Prefer text= or contains= when the element has visible text but no unique CSS class.");
472
500
  }
473
501
  if allowed_actions.contains(&"browser_type") {
474
- rules.push("7c. Use browser_type to enter text/input into form fields or search boxes.");
502
+ rules.push("7c. Use browser_type to enter text into form fields or search boxes. Selector supports the same formats as browser_click.");
475
503
  }
476
504
  if allowed_actions.contains(&"browser_read") {
477
505
  rules.push(
478
506
  "7d. Use browser_read to read the text content of the active page to analyze it.",
479
507
  );
480
508
  }
509
+ if allowed_actions.contains(&"browser_mouse_move") {
510
+ rules.push("7e. Use browser_mouse_move to move the real mouse cursor to absolute (x,y) coordinates.");
511
+ }
512
+ if allowed_actions.contains(&"browser_mouse_click") {
513
+ rules.push("7f. Use browser_mouse_click to perform a native mouse click at (x,y) coordinates. Use browser_screenshot first to find the target position.");
514
+ }
515
+ if allowed_actions.contains(&"browser_key_press") {
516
+ rules.push("7g. Use browser_key_press to press a keyboard key (e.g. Enter, Tab, Escape, ArrowDown). Use after browser_type or browser_mouse_click to interact with focused elements.");
517
+ }
518
+ if allowed_actions.contains(&"browser_screenshot") {
519
+ rules.push("7h. Use browser_screenshot to capture the current page as a PNG image (base64). Use it to inspect the visual state of the page before deciding where to click.");
520
+ }
481
521
  if allowed_actions.contains(&"memory_recall") {
482
522
  rules.push("8. Use memory_recall to search past interactions before asking the user to repeat context.");
483
523
  }
@@ -658,6 +698,14 @@ struct AgentInput {
658
698
  selector: String,
659
699
  #[serde(default)]
660
700
  text: String,
701
+ #[serde(default)]
702
+ x: Option<f64>,
703
+ #[serde(default)]
704
+ y: Option<f64>,
705
+ #[serde(default)]
706
+ button: String,
707
+ #[serde(default)]
708
+ key: String,
661
709
  }
662
710
 
663
711
  #[derive(Debug, Deserialize, Serialize)]
@@ -772,6 +820,7 @@ pub async fn orchestrate_agent_loop<Approve, Progress, Chunk>(
772
820
  task: &str,
773
821
  root: &Path,
774
822
  image_data_uri: Option<String>,
823
+ video_data_uri: Option<String>,
775
824
  chat_id: Option<&str>,
776
825
  agent_id: Option<&str>,
777
826
  fast_mode: bool,
@@ -793,15 +842,17 @@ where
793
842
  ))
794
843
  })?;
795
844
  let resolved_task = resolve_github_links(task, config).await;
796
- let skills = crate::skills::learned_skills_context(Some(&root)).unwrap_or_default();
797
- let mut observation = initial_observation(&resolved_task, &root, &skills);
798
- let mut pending_image = image_data_uri;
799
-
800
- let mut system_prompt = build_system_prompt(config);
801
845
  let chat_id = chat_id
802
846
  .map(str::trim)
803
847
  .filter(|chat_id| !chat_id.is_empty())
804
848
  .unwrap_or(DEFAULT_CONVERSATION_ID);
849
+ let skills =
850
+ crate::skills::learned_skills_context(Some(&root), Some(chat_id)).unwrap_or_default();
851
+ let mut observation = initial_observation(&resolved_task, &root, &skills);
852
+ let mut pending_image = image_data_uri;
853
+ let mut pending_video = video_data_uri;
854
+
855
+ let mut system_prompt = build_system_prompt(config);
805
856
 
806
857
  if let Ok(memory) = MemoryStore::open_default() {
807
858
  let mut profile_instructions = String::new();
@@ -886,6 +937,7 @@ where
886
937
  chat_id: Some(chat_id.to_owned()),
887
938
  image_data_uri: pending_image.take(),
888
939
  audio_data_uri: None,
940
+ video_data_uri: pending_video.take(),
889
941
  document_attachment: None,
890
942
  workspace_path: None,
891
943
  agent_id: None,
@@ -915,6 +967,7 @@ where
915
967
  chat_id: Some(chat_id.to_owned()),
916
968
  image_data_uri: None,
917
969
  audio_data_uri: None,
970
+ video_data_uri: None,
918
971
  document_attachment: None,
919
972
  workspace_path: None,
920
973
  agent_id: None,
@@ -1345,6 +1398,54 @@ where
1345
1398
  .map_err(OrchestrationError::Agent)?;
1346
1399
  Ok(result)
1347
1400
  }
1401
+ "browser_mouse_move" => {
1402
+ let x = input.x.ok_or_else(|| {
1403
+ OrchestrationError::Agent("browser_mouse_move requires 'x'".into())
1404
+ })?;
1405
+ let y = input.y.ok_or_else(|| {
1406
+ OrchestrationError::Agent("browser_mouse_move requires 'y'".into())
1407
+ })?;
1408
+ let result = crate::browser::mouse_move(config, x, y)
1409
+ .await
1410
+ .map_err(OrchestrationError::Agent)?;
1411
+ Ok(result)
1412
+ }
1413
+ "browser_mouse_click" => {
1414
+ let x = input.x.ok_or_else(|| {
1415
+ OrchestrationError::Agent("browser_mouse_click requires 'x'".into())
1416
+ })?;
1417
+ let y = input.y.ok_or_else(|| {
1418
+ OrchestrationError::Agent("browser_mouse_click requires 'y'".into())
1419
+ })?;
1420
+ let button = if input.button.is_empty() {
1421
+ "left"
1422
+ } else {
1423
+ &input.button
1424
+ };
1425
+ let result = crate::browser::mouse_click(config, x, y, button)
1426
+ .await
1427
+ .map_err(OrchestrationError::Agent)?;
1428
+ Ok(result)
1429
+ }
1430
+ "browser_key_press" => {
1431
+ let key = if !input.key.is_empty() {
1432
+ &input.key
1433
+ } else {
1434
+ return Err(OrchestrationError::Agent(
1435
+ "browser_key_press requires 'key'".into(),
1436
+ ));
1437
+ };
1438
+ let result = crate::browser::key_press(config, key)
1439
+ .await
1440
+ .map_err(OrchestrationError::Agent)?;
1441
+ Ok(result)
1442
+ }
1443
+ "browser_screenshot" => {
1444
+ let data = crate::browser::screenshot(config)
1445
+ .await
1446
+ .map_err(OrchestrationError::Agent)?;
1447
+ Ok(format!("data:image/png;base64,{data}"))
1448
+ }
1348
1449
  "memory_recall" => {
1349
1450
  let query = required(&input.query, "query")?;
1350
1451
  let query_lower = query.to_ascii_lowercase();
@@ -1553,6 +1654,25 @@ where
1553
1654
  ApprovalOutcome::Intercepted(obs) => Ok(obs),
1554
1655
  }
1555
1656
  }
1657
+ "mcp_list_tools" => {
1658
+ let server = required(&input.server, "server")?;
1659
+ let approved = approve_cb(&AgentApproval::McpTool {
1660
+ server: server.to_owned(),
1661
+ tool: "list_tools".to_owned(),
1662
+ arguments: serde_json::json!({}),
1663
+ })
1664
+ .map_err(OrchestrationError::Agent)?;
1665
+
1666
+ match approved {
1667
+ ApprovalOutcome::Approved => Ok(serde_json::to_string_pretty(
1668
+ &crate::mcp::list_server_tools(config, server)
1669
+ .map_err(|e| OrchestrationError::Agent(e.to_string()))?,
1670
+ )
1671
+ .map_err(|e| OrchestrationError::Agent(e.to_string()))?),
1672
+ ApprovalOutcome::Denied => Ok(format!("User denied MCP list tools: {}", server)),
1673
+ ApprovalOutcome::Intercepted(obs) => Ok(obs),
1674
+ }
1675
+ }
1556
1676
  "run_shell" => {
1557
1677
  let command = required(&input.command, "command")?;
1558
1678
  let mode = classify_shell_command(command).mode.as_str().to_owned();
@@ -2260,6 +2380,7 @@ Example response:
2260
2380
  chat_id: None,
2261
2381
  image_data_uri: None,
2262
2382
  audio_data_uri: None,
2383
+ video_data_uri: None,
2263
2384
  document_attachment: None,
2264
2385
  workspace_path: None,
2265
2386
  agent_id: None,
@@ -2320,6 +2441,7 @@ mod tests {
2320
2441
  chat_id: None,
2321
2442
  image_data_uri: None,
2322
2443
  audio_data_uri: None,
2444
+ video_data_uri: None,
2323
2445
  document_attachment: None,
2324
2446
  workspace_path: None,
2325
2447
  agent_id: None,
@@ -2,6 +2,7 @@ use std::{
2
2
  fs,
3
3
  io::Cursor,
4
4
  path::{Path, PathBuf},
5
+ process::Command,
5
6
  time::{SystemTime, UNIX_EPOCH},
6
7
  };
7
8
 
@@ -131,6 +132,11 @@ pub fn parse_data_uri(raw: &str) -> Option<(String, &'static str, Vec<u8>)> {
131
132
  "image/jpeg" | "image/jpg" => "jpg",
132
133
  "image/webp" => "webp",
133
134
  "image/gif" => "gif",
135
+ "video/mp4" => "mp4",
136
+ "video/webm" => "webm",
137
+ "video/quicktime" => "mov",
138
+ "video/x-msvideo" => "avi",
139
+ "video/x-matroska" => "mkv",
134
140
  _ => return None,
135
141
  };
136
142
  Some((mime_type, extension, STANDARD.decode(encoded).ok()?))
@@ -166,7 +172,13 @@ fn save_chat_images_to_directory(
166
172
  let id = filename
167
173
  .trim_end_matches(&format!(".{extension}"))
168
174
  .to_owned();
169
- let thumbnail_path = create_thumbnail_from_bytes(directory, &id, &bytes);
175
+ let thumbnail_path = if mime_type.starts_with("image/") {
176
+ create_thumbnail_from_bytes(directory, &id, &bytes)
177
+ } else if mime_type.starts_with("video/") {
178
+ extract_video_thumbnail_from_bytes(directory, &id, &bytes)
179
+ } else {
180
+ None
181
+ };
170
182
  let entry = PictureEntry {
171
183
  id,
172
184
  filename,
@@ -221,6 +233,57 @@ fn create_thumbnail_from_bytes(directory: &Path, id: &str, bytes: &[u8]) -> Opti
221
233
  Some(path)
222
234
  }
223
235
 
236
+ fn extract_video_thumbnail_from_bytes(directory: &Path, id: &str, bytes: &[u8]) -> Option<PathBuf> {
237
+ // Write video to temporary file
238
+ let temp_video = std::env::temp_dir().join(format!("mint-video-{}.tmp", id));
239
+ if fs::write(&temp_video, bytes).is_err() {
240
+ return None;
241
+ }
242
+
243
+ let temp_frame = std::env::temp_dir().join(format!("mint-frame-{}.png", id));
244
+
245
+ let res = (|| -> Option<PathBuf> {
246
+ let temp_video_str = temp_video.to_str()?;
247
+ let temp_frame_str = temp_frame.to_str()?;
248
+
249
+ let output = Command::new("ffmpeg")
250
+ .args(&["-i", temp_video_str])
251
+ .args(&["-vf", "select=eq(n\\,0)"])
252
+ .args(&["-vframes", "1"])
253
+ .args(&["-y", temp_frame_str])
254
+ .stderr(std::process::Stdio::null())
255
+ .stdout(std::process::Stdio::null())
256
+ .output()
257
+ .ok()?;
258
+
259
+ if !output.status.success() {
260
+ return None;
261
+ }
262
+
263
+ // Load extracted frame and create thumbnail
264
+ let frame_bytes = fs::read(&temp_frame).ok()?;
265
+ let image = image::load_from_memory(&frame_bytes).ok()?;
266
+ let thumbnail = image.thumbnail(THUMBNAIL_MAX_SIZE, THUMBNAIL_MAX_SIZE);
267
+
268
+ let thumbnail_directory = thumbnails_directory(directory);
269
+ ensure_directory(&thumbnail_directory).ok()?;
270
+ let path = thumbnail_directory.join(format!("{id}.thumb.png"));
271
+
272
+ let mut encoded = Cursor::new(Vec::new());
273
+ thumbnail
274
+ .write_to(&mut encoded, image::ImageFormat::Png)
275
+ .ok()?;
276
+ fs::write(&path, encoded.into_inner()).ok()?;
277
+ Some(path)
278
+ })();
279
+
280
+ // Always clean up temp files to avoid leaking in /tmp
281
+ let _ = fs::remove_file(&temp_video);
282
+ let _ = fs::remove_file(&temp_frame);
283
+
284
+ res
285
+ }
286
+
224
287
  fn read_index(directory: &Path) -> Result<Vec<PictureEntry>, PictureError> {
225
288
  let path = directory.join("pictures.json");
226
289
  if !path.exists() {
@@ -53,10 +53,38 @@ pub fn learn_skill(path: &Path) -> Result<LearnedSkill, SkillError> {
53
53
  Ok(MemoryStore::open_default()?.add_learned_skill(name, &path.to_string_lossy(), &content)?)
54
54
  }
55
55
 
56
- pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, SkillError> {
56
+ pub fn load_agent_rules_file(file_path: &Path, list: &mut Vec<LearnedSkill>) {
57
+ if file_path.is_file() {
58
+ if let Ok(content) = fs::read_to_string(file_path) {
59
+ let name = file_path
60
+ .file_name()
61
+ .and_then(|n| n.to_str())
62
+ .unwrap_or("AGENTS.md")
63
+ .to_string();
64
+ let description = parse_skill_description(&content)
65
+ .or_else(|| Some("Workspace instructions and rules".to_string()));
66
+ list.push(LearnedSkill {
67
+ id: 0,
68
+ name,
69
+ source_path: file_path.to_string_lossy().to_string(),
70
+ content,
71
+ created_at: String::new(),
72
+ description,
73
+ });
74
+ }
75
+ }
76
+ }
77
+
78
+ pub fn learned_skills_context(
79
+ workspace_root: Option<&Path>,
80
+ chat_id: Option<&str>,
81
+ ) -> Result<String, SkillError> {
57
82
  let mut skills = MemoryStore::open_default()?.learned_skills(20)?;
58
83
 
59
84
  if let Some(home) = dirs::home_dir() {
85
+ let global_agents_path = home.join(".gemini").join("config").join("AGENTS.md");
86
+ load_agent_rules_file(&global_agents_path, &mut skills);
87
+
60
88
  let global_skills_path = home.join(".config").join("mint").join("mint-skills");
61
89
  if !global_skills_path.exists() {
62
90
  let _ = std::fs::create_dir_all(&global_skills_path);
@@ -65,6 +93,12 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
65
93
  }
66
94
 
67
95
  if let Some(root) = workspace_root {
96
+ let workspace_agents_path1 = root.join(".agents").join("AGENTS.md");
97
+ load_agent_rules_file(&workspace_agents_path1, &mut skills);
98
+
99
+ let workspace_agents_path2 = root.join("AGENTS.md");
100
+ load_agent_rules_file(&workspace_agents_path2, &mut skills);
101
+
68
102
  let workspace_skills_path1 = root.join(".agents").join("skills");
69
103
  load_skills_from_dir(&workspace_skills_path1, &mut skills);
70
104
 
@@ -77,11 +111,53 @@ pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, S
77
111
  unique_skills.insert(skill.name.clone(), skill);
78
112
  }
79
113
 
80
- let value = unique_skills
81
- .into_values()
82
- .map(|skill| format!("Skill: {}\n{}", skill.name, skill.content))
83
- .collect::<Vec<_>>()
84
- .join("\n\n");
114
+ let mut has_history = false;
115
+ if let Some(cid) = chat_id {
116
+ if let Ok(memory) = MemoryStore::open_default() {
117
+ if let Ok(interactions) = memory.recent_interactions_for_chat(cid, 1) {
118
+ has_history = !interactions.is_empty();
119
+ }
120
+ }
121
+ }
122
+
123
+ let mut formatted_skills = Vec::new();
124
+ for skill in unique_skills.into_values() {
125
+ let mut is_workspace_skill = false;
126
+ let mut rel_path = String::new();
127
+ if let Some(root) = workspace_root {
128
+ let skill_path = Path::new(&skill.source_path);
129
+ if let Ok(canonical_skill) = skill_path.canonicalize() {
130
+ if let Ok(canonical_root) = root.canonicalize() {
131
+ if canonical_skill.starts_with(&canonical_root) {
132
+ if let Ok(relative) = canonical_skill.strip_prefix(&canonical_root) {
133
+ is_workspace_skill = true;
134
+ rel_path = relative.to_string_lossy().to_string();
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ if is_workspace_skill {
142
+ let desc = skill
143
+ .description
144
+ .as_deref()
145
+ .unwrap_or("No description provided");
146
+ let status_msg = if has_history {
147
+ "READ. You have already read this skill in a previous turn of this conversation. Do NOT call `read_file` to read it again unless you need to re-verify its contents."
148
+ } else {
149
+ "UNREAD. You MUST execute the `read_file` tool with the path above to read this skill before starting the task."
150
+ };
151
+ formatted_skills.push(format!(
152
+ "Skill: {}\nDescription: {}\nPath: {}\nStatus: {}",
153
+ skill.name, desc, rel_path, status_msg
154
+ ));
155
+ } else {
156
+ formatted_skills.push(format!("Skill: {}\n{}", skill.name, skill.content));
157
+ }
158
+ }
159
+
160
+ let value = formatted_skills.join("\n\n");
85
161
  if value.len() <= MAX_CONTEXT_BYTES {
86
162
  return Ok(value);
87
163
  }