@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.
- package/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +285 -140
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +287 -43
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +336 -2854
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +44 -1
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +376 -13
- package/crates/mint-core/src/browser.rs +629 -101
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +52 -4
- package/crates/mint-core/src/code_tools.rs +29 -11
- package/crates/mint-core/src/config.rs +118 -0
- package/crates/mint-core/src/image_gen.rs +294 -33
- package/crates/mint-core/src/lib.rs +5 -2
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/orchestration.rs +133 -11
- package/crates/mint-core/src/pictures.rs +64 -1
- package/crates/mint-core/src/skills.rs +82 -6
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +461 -64
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +150 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +38 -2
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +352 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +353 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/lib.rs +114 -8
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
}
|