@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
|
@@ -2,7 +2,6 @@ use anyhow::Result;
|
|
|
2
2
|
use clap::{Parser, Subcommand};
|
|
3
3
|
use std::{
|
|
4
4
|
fs,
|
|
5
|
-
io::{self, Write},
|
|
6
5
|
path::{Path, PathBuf},
|
|
7
6
|
};
|
|
8
7
|
|
|
@@ -12,41 +11,98 @@ use mint_core::{
|
|
|
12
11
|
build_symbol_index, classify_shell_command, config_path, create_folder, execute_native_plugin,
|
|
13
12
|
fetch_github_repo_summary, find_paths, generate_images, index_semantic_code, initialize_config,
|
|
14
13
|
inspect_code_plan, list_code_files, load_config, native_plugins,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
search_semantic_code, set_config_value,
|
|
14
|
+
orchestrate_chat_with_fallback, parse_github_url, propose_code_edits, read_code_file,
|
|
15
|
+
repository_summary, run_shell_command, search_code, search_semantic_code, set_config_value,
|
|
18
16
|
};
|
|
19
17
|
|
|
18
|
+
mod actions;
|
|
20
19
|
mod agent;
|
|
21
20
|
mod gmail;
|
|
22
21
|
mod image;
|
|
22
|
+
mod interactive;
|
|
23
|
+
mod markdown;
|
|
23
24
|
mod mcp;
|
|
24
25
|
mod onboard;
|
|
25
26
|
mod setup;
|
|
26
27
|
mod skills;
|
|
27
28
|
mod updater;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
30
|
+
pub use interactive::{
|
|
31
|
+
SESSION_APPROVED, active_model, confirm, print_welcome_banner, run_interactive_chat,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
pub const RESET: &str = "\x1b[0m";
|
|
35
|
+
pub const MINT: &str = "\x1b[32m";
|
|
36
|
+
pub const BLUE: &str = "\x1b[38;2;78;201;216m";
|
|
37
|
+
pub const DIM: &str = "\x1b[90m";
|
|
38
|
+
pub const ERROR: &str = "\x1b[31m";
|
|
39
|
+
pub const WARN: &str = "\x1b[33m";
|
|
40
|
+
pub const COMPOSER_BG: &str = "\x1b[48;2;35;39;45m";
|
|
36
41
|
|
|
37
|
-
async fn run_code_agent_with_saved_image(
|
|
42
|
+
pub(crate) async fn run_code_agent_with_saved_image(
|
|
38
43
|
task: &str,
|
|
39
44
|
current_dir: &Path,
|
|
40
45
|
config: &MintConfig,
|
|
41
46
|
image_data_uri: Option<String>,
|
|
47
|
+
video_data_uri: Option<String>,
|
|
42
48
|
options: agent::AgentOptions,
|
|
43
49
|
) -> Result<()> {
|
|
44
50
|
let sent_image = image_data_uri.clone();
|
|
45
|
-
|
|
51
|
+
let sent_video = video_data_uri.clone();
|
|
52
|
+
agent::run_code_agent_with_options(
|
|
53
|
+
task,
|
|
54
|
+
current_dir,
|
|
55
|
+
config,
|
|
56
|
+
image_data_uri,
|
|
57
|
+
video_data_uri,
|
|
58
|
+
options,
|
|
59
|
+
)
|
|
60
|
+
.await?;
|
|
61
|
+
// Save any attached images and videos that were sent with the task
|
|
46
62
|
image::save_sent_image_after_send(sent_image.as_deref(), task);
|
|
63
|
+
image::save_sent_image_after_send(sent_video.as_deref(), task);
|
|
47
64
|
Ok(())
|
|
48
65
|
}
|
|
49
66
|
|
|
67
|
+
fn configured(config: &mint_core::MintConfig, keys: &[&str]) -> bool {
|
|
68
|
+
keys.iter().all(|key| {
|
|
69
|
+
config
|
|
70
|
+
.extra
|
|
71
|
+
.get(*key)
|
|
72
|
+
.and_then(serde_json::Value::as_str)
|
|
73
|
+
.is_some_and(|value| !value.trim().is_empty())
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn edit_content(
|
|
78
|
+
content: Option<String>,
|
|
79
|
+
from_file: Option<PathBuf>,
|
|
80
|
+
config: &MintConfig,
|
|
81
|
+
) -> Result<String> {
|
|
82
|
+
match from_file {
|
|
83
|
+
Some(path) => {
|
|
84
|
+
let path = assert_path_capability(&path, Capability::Read, config)?;
|
|
85
|
+
Ok(fs::read_to_string(path)?)
|
|
86
|
+
}
|
|
87
|
+
None => Ok(content.unwrap_or_default()),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn file_edits(values: &[String], config: &MintConfig) -> Result<Vec<CodeEdit>> {
|
|
92
|
+
values
|
|
93
|
+
.iter()
|
|
94
|
+
.map(|value| {
|
|
95
|
+
let (target, source) = value
|
|
96
|
+
.split_once('=')
|
|
97
|
+
.ok_or_else(|| anyhow::anyhow!("edit must use TARGET=SOURCE format"))?;
|
|
98
|
+
Ok(CodeEdit {
|
|
99
|
+
path: PathBuf::from(target),
|
|
100
|
+
content: edit_content(None, Some(PathBuf::from(source)), config)?,
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
.collect()
|
|
104
|
+
}
|
|
105
|
+
|
|
50
106
|
#[derive(Debug, Parser)]
|
|
51
107
|
#[command(name = "mint", version, about = "Mint native CLI")]
|
|
52
108
|
struct Cli {
|
|
@@ -199,6 +255,23 @@ enum Command {
|
|
|
199
255
|
#[arg(long)]
|
|
200
256
|
output: Option<PathBuf>,
|
|
201
257
|
},
|
|
258
|
+
/// Generate a video from a text prompt using Google Veo.
|
|
259
|
+
Veo {
|
|
260
|
+
/// Text description of the video to generate.
|
|
261
|
+
prompt: String,
|
|
262
|
+
/// Aspect ratio: 16:9, 9:16, or 1:1 [default: 16:9]
|
|
263
|
+
#[arg(long, default_value = "16:9")]
|
|
264
|
+
aspect: String,
|
|
265
|
+
/// Duration of the video in seconds (5 or 8) [default: 5]
|
|
266
|
+
#[arg(long, default_value_t = 5)]
|
|
267
|
+
duration: u32,
|
|
268
|
+
/// Negative prompt — elements to avoid in the video
|
|
269
|
+
#[arg(long)]
|
|
270
|
+
negative: Option<String>,
|
|
271
|
+
/// Save the generated video to this path
|
|
272
|
+
#[arg(long)]
|
|
273
|
+
output: Option<PathBuf>,
|
|
274
|
+
},
|
|
202
275
|
}
|
|
203
276
|
|
|
204
277
|
#[derive(Debug, Subcommand)]
|
|
@@ -455,7 +528,9 @@ enum MemoryCommand {
|
|
|
455
528
|
},
|
|
456
529
|
}
|
|
457
530
|
|
|
458
|
-
fn print_mcp_servers(
|
|
531
|
+
pub(crate) fn print_mcp_servers(
|
|
532
|
+
servers: &std::collections::BTreeMap<String, mint_core::mcp::McpServer>,
|
|
533
|
+
) {
|
|
459
534
|
if servers.is_empty() {
|
|
460
535
|
println!("{DIM}(No MCP servers configured.){RESET}\n");
|
|
461
536
|
return;
|
|
@@ -474,6 +549,7 @@ fn print_mcp_servers(servers: &std::collections::BTreeMap<String, mint_core::mcp
|
|
|
474
549
|
async fn main() -> Result<()> {
|
|
475
550
|
match Cli::parse().command {
|
|
476
551
|
None => {
|
|
552
|
+
mint_core::channels::start_channels();
|
|
477
553
|
run_interactive_chat().await?;
|
|
478
554
|
}
|
|
479
555
|
Some(cmd) => match cmd {
|
|
@@ -541,8 +617,9 @@ async fn main() -> Result<()> {
|
|
|
541
617
|
run_cli_agent_task(task).await?;
|
|
542
618
|
}
|
|
543
619
|
Command::Auto => {
|
|
544
|
-
|
|
545
|
-
|
|
620
|
+
let config = load_config()?;
|
|
621
|
+
print_welcome_banner(&config);
|
|
622
|
+
println!("\n🚀 Starting Mint Browser Automation Environment...");
|
|
546
623
|
mint_core::spawn_automation_browser(&config)
|
|
547
624
|
.await
|
|
548
625
|
.map_err(anyhow::Error::msg)?;
|
|
@@ -555,6 +632,10 @@ async fn main() -> Result<()> {
|
|
|
555
632
|
"browser_click",
|
|
556
633
|
"browser_type",
|
|
557
634
|
"browser_read",
|
|
635
|
+
"browser_mouse_move",
|
|
636
|
+
"browser_mouse_click",
|
|
637
|
+
"browser_key_press",
|
|
638
|
+
"browser_screenshot",
|
|
558
639
|
] {
|
|
559
640
|
if config_mut.disabled_tools.contains(&tool.to_string()) {
|
|
560
641
|
config_mut.disabled_tools.retain(|x| x != *tool);
|
|
@@ -564,7 +645,7 @@ async fn main() -> Result<()> {
|
|
|
564
645
|
if changed {
|
|
565
646
|
mint_core::save_config(&config_mut)?;
|
|
566
647
|
println!(
|
|
567
|
-
"✅ Enabled browser automation tools in config: browser_open, browser_click, browser_type, browser_read"
|
|
648
|
+
"✅ Enabled browser automation tools in config: browser_open, browser_click, browser_type, browser_read, browser_mouse_move, browser_mouse_click, browser_key_press, browser_screenshot"
|
|
568
649
|
);
|
|
569
650
|
}
|
|
570
651
|
|
|
@@ -613,6 +694,22 @@ async fn main() -> Result<()> {
|
|
|
613
694
|
|| line_str.contains("[READ_SUCCESS]")
|
|
614
695
|
{
|
|
615
696
|
println!("📖 {line_str}");
|
|
697
|
+
} else if line_str.contains("[MOUSE_MOVE]")
|
|
698
|
+
|| line_str.contains("[MOUSE_MOVE_SUCCESS]")
|
|
699
|
+
{
|
|
700
|
+
println!("🖱️ {line_str}");
|
|
701
|
+
} else if line_str.contains("[MOUSE_CLICK]")
|
|
702
|
+
|| line_str.contains("[MOUSE_CLICK_SUCCESS]")
|
|
703
|
+
{
|
|
704
|
+
println!("🖱️ ● {line_str}");
|
|
705
|
+
} else if line_str.contains("[KEY_PRESS]")
|
|
706
|
+
|| line_str.contains("[KEY_PRESS_SUCCESS]")
|
|
707
|
+
{
|
|
708
|
+
println!("⌨️ {line_str}");
|
|
709
|
+
} else if line_str.contains("[SCREENSHOT]")
|
|
710
|
+
|| line_str.contains("[SCREENSHOT_SUCCESS]")
|
|
711
|
+
{
|
|
712
|
+
println!("📸 {line_str}");
|
|
616
713
|
} else if line_str.contains("_ERROR]") {
|
|
617
714
|
println!("❌ {line_str}");
|
|
618
715
|
} else {
|
|
@@ -634,6 +731,40 @@ async fn main() -> Result<()> {
|
|
|
634
731
|
launch_mint_target("web".into()).await?;
|
|
635
732
|
}
|
|
636
733
|
Command::Api { port } => {
|
|
734
|
+
let config = load_config()?;
|
|
735
|
+
print_welcome_banner(&config);
|
|
736
|
+
|
|
737
|
+
println!("\n{MINT}✔ Mint API Server is running!{RESET}\n");
|
|
738
|
+
println!(
|
|
739
|
+
" {BLUE}API Server URL:{RESET} {MINT}http://localhost:{}{RESET}\n",
|
|
740
|
+
port
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
println!("Messaging Bridges Status:");
|
|
744
|
+
|
|
745
|
+
let bridges = [
|
|
746
|
+
("enableTelegramBridge", "Telegram Bot Bridge"),
|
|
747
|
+
("enableDiscordBridge", "Discord Bot Bridge"),
|
|
748
|
+
("enableSlackBridge", "Slack Bot Bridge"),
|
|
749
|
+
("enableLineBridge", "LINE Bot Bridge"),
|
|
750
|
+
("enableWhatsappBridge", "WhatsApp Cloud Bridge"),
|
|
751
|
+
];
|
|
752
|
+
|
|
753
|
+
for &(key, name) in &bridges {
|
|
754
|
+
let enabled = config
|
|
755
|
+
.extra
|
|
756
|
+
.get(key)
|
|
757
|
+
.and_then(|v| v.as_bool())
|
|
758
|
+
.unwrap_or(false);
|
|
759
|
+
if enabled {
|
|
760
|
+
println!(" {MINT}● {name:<23} [Active]{RESET}");
|
|
761
|
+
} else {
|
|
762
|
+
println!(" {DIM}○ {name:<23} [Inactive]{RESET}");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
println!("\n{DIM}(API Requests & Error logs will be displayed below){RESET}\n");
|
|
767
|
+
println!("{DIM}Press Ctrl+C to stop{RESET}\n");
|
|
637
768
|
mint_core::start_api_server(port).await?;
|
|
638
769
|
}
|
|
639
770
|
Command::Mcp { command } => match command {
|
|
@@ -675,14 +806,26 @@ async fn main() -> Result<()> {
|
|
|
675
806
|
server,
|
|
676
807
|
tool,
|
|
677
808
|
arguments,
|
|
678
|
-
} =>
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
809
|
+
} => {
|
|
810
|
+
use indicatif::{ProgressBar, ProgressStyle};
|
|
811
|
+
use std::time::Duration;
|
|
812
|
+
let spinner = ProgressBar::new_spinner();
|
|
813
|
+
spinner.set_style(
|
|
814
|
+
ProgressStyle::default_spinner()
|
|
815
|
+
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
816
|
+
.template("{spinner:.green} {msg}")
|
|
817
|
+
.unwrap(),
|
|
818
|
+
);
|
|
819
|
+
spinner.set_message(format!(
|
|
820
|
+
"Executing MCP tool '{}' on server '{}'...",
|
|
821
|
+
tool, server
|
|
822
|
+
));
|
|
823
|
+
spinner.enable_steady_tick(Duration::from_millis(80));
|
|
824
|
+
|
|
825
|
+
let res = mcp::call(&server, &tool, serde_json::from_str(&arguments)?);
|
|
826
|
+
spinner.finish_and_clear();
|
|
827
|
+
println!("{}", serde_json::to_string_pretty(&res?)?);
|
|
828
|
+
}
|
|
686
829
|
},
|
|
687
830
|
Command::Gmail { command } => match command {
|
|
688
831
|
GmailCommand::Auth { no_open, port } => gmail::auth(no_open, port).await?,
|
|
@@ -752,17 +895,39 @@ async fn main() -> Result<()> {
|
|
|
752
895
|
anyhow::bail!("use mint learn <path>, --list, or --delete <id|path|name>");
|
|
753
896
|
}
|
|
754
897
|
}
|
|
755
|
-
Command::Symbols { root, limit } =>
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
898
|
+
Command::Symbols { root, limit } => {
|
|
899
|
+
use indicatif::{ProgressBar, ProgressStyle};
|
|
900
|
+
use std::time::Duration;
|
|
901
|
+
let spinner = ProgressBar::new_spinner();
|
|
902
|
+
spinner.set_style(
|
|
903
|
+
ProgressStyle::default_spinner()
|
|
904
|
+
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
905
|
+
.template("{spinner:.green} {msg}")
|
|
906
|
+
.unwrap(),
|
|
907
|
+
);
|
|
908
|
+
spinner.set_message("Building symbol index...");
|
|
909
|
+
spinner.enable_steady_tick(Duration::from_millis(80));
|
|
910
|
+
let index = build_symbol_index(&root, limit, &load_config()?);
|
|
911
|
+
spinner.finish_and_clear();
|
|
912
|
+
println!("{}", serde_json::to_string_pretty(&index?)?);
|
|
913
|
+
}
|
|
759
914
|
Command::SemanticCode { command } => match command {
|
|
760
|
-
SemanticCodeCommand::Index { root } =>
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
915
|
+
SemanticCodeCommand::Index { root } => {
|
|
916
|
+
use indicatif::{ProgressBar, ProgressStyle};
|
|
917
|
+
use std::time::Duration;
|
|
918
|
+
let spinner = ProgressBar::new_spinner();
|
|
919
|
+
spinner.set_style(
|
|
920
|
+
ProgressStyle::default_spinner()
|
|
921
|
+
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
922
|
+
.template("{spinner:.green} {msg}")
|
|
923
|
+
.unwrap(),
|
|
924
|
+
);
|
|
925
|
+
spinner.set_message("Indexing semantic code workspace...");
|
|
926
|
+
spinner.enable_steady_tick(Duration::from_millis(80));
|
|
927
|
+
let res = index_semantic_code(&root, &load_config()?).await;
|
|
928
|
+
spinner.finish_and_clear();
|
|
929
|
+
println!("{}", serde_json::to_string_pretty(&res?)?);
|
|
930
|
+
}
|
|
766
931
|
SemanticCodeCommand::Search { query, root, limit } => println!(
|
|
767
932
|
"{}",
|
|
768
933
|
serde_json::to_string_pretty(
|
|
@@ -786,6 +951,7 @@ async fn main() -> Result<()> {
|
|
|
786
951
|
&std::env::current_dir()?,
|
|
787
952
|
&load_config()?,
|
|
788
953
|
image_data_uri,
|
|
954
|
+
None,
|
|
789
955
|
agent::AgentOptions::default(),
|
|
790
956
|
)
|
|
791
957
|
.await?;
|
|
@@ -798,6 +964,7 @@ async fn main() -> Result<()> {
|
|
|
798
964
|
chat_id: Some(CHAT_CLI_ID.to_owned()),
|
|
799
965
|
image_data_uri,
|
|
800
966
|
audio_data_uri: None,
|
|
967
|
+
video_data_uri: None,
|
|
801
968
|
document_attachment: None,
|
|
802
969
|
workspace_path: None,
|
|
803
970
|
agent_id: None,
|
|
@@ -853,7 +1020,7 @@ async fn main() -> Result<()> {
|
|
|
853
1020
|
command,
|
|
854
1021
|
} => {
|
|
855
1022
|
let output = run_shell_command(&command.join(" "), &cwd, approve, &load_config()?)?;
|
|
856
|
-
print_shell_output(&output);
|
|
1023
|
+
actions::print_shell_output(&output);
|
|
857
1024
|
if !output.success {
|
|
858
1025
|
anyhow::bail!(
|
|
859
1026
|
"shell command exited with status {}",
|
|
@@ -1080,16 +1247,16 @@ async fn main() -> Result<()> {
|
|
|
1080
1247
|
}
|
|
1081
1248
|
}
|
|
1082
1249
|
Command::Open { target } => {
|
|
1083
|
-
open_system_handler(&target)?;
|
|
1250
|
+
actions::open_system_handler(&target)?;
|
|
1084
1251
|
}
|
|
1085
1252
|
Command::OpenApp { name } => {
|
|
1086
|
-
launch_desktop_app(&name)?;
|
|
1253
|
+
actions::launch_desktop_app(&name)?;
|
|
1087
1254
|
}
|
|
1088
1255
|
Command::ReadFile { path } => {
|
|
1089
|
-
read_file_content(&path)?;
|
|
1256
|
+
actions::read_file_content(&path)?;
|
|
1090
1257
|
}
|
|
1091
1258
|
Command::ReadFolder { path } => {
|
|
1092
|
-
read_folder_content(&path)?;
|
|
1259
|
+
actions::read_folder_content(&path)?;
|
|
1093
1260
|
}
|
|
1094
1261
|
Command::Onboard => {
|
|
1095
1262
|
onboard::run().await?;
|
|
@@ -1108,8 +1275,18 @@ async fn main() -> Result<()> {
|
|
|
1108
1275
|
} => {
|
|
1109
1276
|
let config = load_config()?;
|
|
1110
1277
|
let count = count.clamp(1, 4);
|
|
1111
|
-
|
|
1112
|
-
|
|
1278
|
+
use indicatif::{ProgressBar, ProgressStyle};
|
|
1279
|
+
use std::time::Duration;
|
|
1280
|
+
let spinner = ProgressBar::new_spinner();
|
|
1281
|
+
spinner.set_style(
|
|
1282
|
+
ProgressStyle::default_spinner()
|
|
1283
|
+
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
1284
|
+
.template("{spinner:.green} {msg}")
|
|
1285
|
+
.unwrap(),
|
|
1286
|
+
);
|
|
1287
|
+
spinner.set_message(format!("Generating {} image(s)...", count));
|
|
1288
|
+
spinner.enable_steady_tick(Duration::from_millis(80));
|
|
1289
|
+
|
|
1113
1290
|
let request = ImageGenRequest {
|
|
1114
1291
|
prompt: prompt.clone(),
|
|
1115
1292
|
negative_prompt: negative,
|
|
@@ -1117,13 +1294,14 @@ async fn main() -> Result<()> {
|
|
|
1117
1294
|
num_images: Some(count),
|
|
1118
1295
|
model: None,
|
|
1119
1296
|
provider: None,
|
|
1297
|
+
image_data_uri: None,
|
|
1298
|
+
mask_data_uri: None,
|
|
1299
|
+
mode: None,
|
|
1120
1300
|
};
|
|
1121
1301
|
match generate_images(&config, &request).await {
|
|
1122
1302
|
Ok(result) => {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
result.images.len()
|
|
1126
|
-
);
|
|
1303
|
+
spinner.finish_and_clear();
|
|
1304
|
+
eprintln!("{MINT}✦ Generated {} image(s){RESET}", result.images.len());
|
|
1127
1305
|
let data_uris: Vec<String> = result
|
|
1128
1306
|
.images
|
|
1129
1307
|
.iter()
|
|
@@ -1160,11 +1338,73 @@ async fn main() -> Result<()> {
|
|
|
1160
1338
|
}
|
|
1161
1339
|
}
|
|
1162
1340
|
Err(e) => {
|
|
1341
|
+
spinner.finish_and_clear();
|
|
1163
1342
|
eprintln!("{ERROR}✗ Image generation failed: {e}{RESET}");
|
|
1164
1343
|
anyhow::bail!("image generation failed: {e}");
|
|
1165
1344
|
}
|
|
1166
1345
|
}
|
|
1167
1346
|
}
|
|
1347
|
+
Command::Veo {
|
|
1348
|
+
prompt,
|
|
1349
|
+
aspect,
|
|
1350
|
+
duration,
|
|
1351
|
+
negative,
|
|
1352
|
+
output,
|
|
1353
|
+
} => {
|
|
1354
|
+
let config = load_config()?;
|
|
1355
|
+
use indicatif::{ProgressBar, ProgressStyle};
|
|
1356
|
+
use std::time::Duration as StdDuration;
|
|
1357
|
+
let spinner = ProgressBar::new_spinner();
|
|
1358
|
+
spinner.set_style(
|
|
1359
|
+
ProgressStyle::default_spinner()
|
|
1360
|
+
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
1361
|
+
.template("{spinner:.magenta} {msg}")
|
|
1362
|
+
.unwrap(),
|
|
1363
|
+
);
|
|
1364
|
+
spinner.set_message(format!(
|
|
1365
|
+
"Generating {}s (aspect: {}) video with Veo for: \"{}\"...",
|
|
1366
|
+
duration, aspect, prompt
|
|
1367
|
+
));
|
|
1368
|
+
spinner.enable_steady_tick(StdDuration::from_millis(80));
|
|
1369
|
+
|
|
1370
|
+
let gen_request = mint_core::VideoGenRequest {
|
|
1371
|
+
prompt: prompt.clone(),
|
|
1372
|
+
negative_prompt: negative,
|
|
1373
|
+
aspect_ratio: aspect.clone(),
|
|
1374
|
+
duration,
|
|
1375
|
+
model: None,
|
|
1376
|
+
provider: "veo".to_string(),
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
match mint_core::generate_video(&config, &gen_request).await {
|
|
1380
|
+
Ok(result) => {
|
|
1381
|
+
spinner.finish_and_clear();
|
|
1382
|
+
if let Some(video) = result.videos.first() {
|
|
1383
|
+
println!("{MINT}✓ Video generated successfully!{RESET}");
|
|
1384
|
+
println!("{MINT}Saved to: {}{RESET}", video.path.display());
|
|
1385
|
+
if let Some(out_path) = output {
|
|
1386
|
+
if let Err(e) = std::fs::copy(&video.path, &out_path) {
|
|
1387
|
+
eprintln!(
|
|
1388
|
+
"{ERROR}✗ Failed to copy generated video to output path: {e}{RESET}"
|
|
1389
|
+
);
|
|
1390
|
+
} else {
|
|
1391
|
+
println!(
|
|
1392
|
+
"{MINT}✓ Copied to output destination: {}{RESET}",
|
|
1393
|
+
out_path.display()
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
eprintln!("{ERROR}✗ Video generation returned no videos.{RESET}");
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
Err(e) => {
|
|
1402
|
+
spinner.finish_and_clear();
|
|
1403
|
+
eprintln!("{ERROR}✗ Video generation failed: {e}{RESET}");
|
|
1404
|
+
anyhow::bail!("video generation failed: {e}");
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1168
1408
|
},
|
|
1169
1409
|
}
|
|
1170
1410
|
Ok(())
|
|
@@ -1180,12 +1420,11 @@ async fn launch_mint_target(target: String) -> Result<()> {
|
|
|
1180
1420
|
const APP_URL: &str = "https://mint.aemeth.xyz";
|
|
1181
1421
|
println!("{MINT}Opening Mint App Link...{RESET}");
|
|
1182
1422
|
println!("{BLUE}Open app:{RESET} {APP_URL}\n");
|
|
1183
|
-
open_system_handler(APP_URL)?;
|
|
1423
|
+
actions::open_system_handler(APP_URL)?;
|
|
1184
1424
|
}
|
|
1185
1425
|
"web" => {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
);
|
|
1426
|
+
let config = load_config()?;
|
|
1427
|
+
print_welcome_banner(&config);
|
|
1189
1428
|
let project_root = {
|
|
1190
1429
|
let mut found = None;
|
|
1191
1430
|
if let Ok(exe_path) = std::env::current_exe() {
|
|
@@ -1222,384 +1461,27 @@ async fn launch_mint_target(target: String) -> Result<()> {
|
|
|
1222
1461
|
.spawn()
|
|
1223
1462
|
.map_err(|e| anyhow::anyhow!("Failed to launch web app: {e}"))?;
|
|
1224
1463
|
|
|
1225
|
-
println!("{MINT}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
"{BLUE}Please open {RESET}{}{BLUE} in your web browser to access the Mint Web UI.{RESET}\n",
|
|
1233
|
-
local_ip_msg
|
|
1234
|
-
);
|
|
1235
|
-
mint_core::start_api_server(3000).await?;
|
|
1236
|
-
}
|
|
1237
|
-
_ => {}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
Ok(())
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
fn is_table_line(line: &str) -> bool {
|
|
1244
|
-
let trimmed = line.trim();
|
|
1245
|
-
trimmed.starts_with('|') && trimmed.ends_with('|') && trimmed.len() > 1
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
fn process_inline_bold(s: &str) -> String {
|
|
1249
|
-
let mut result = String::new();
|
|
1250
|
-
let parts = s.split("**");
|
|
1251
|
-
let mut is_bold = false;
|
|
1252
|
-
for part in parts {
|
|
1253
|
-
if is_bold {
|
|
1254
|
-
result.push_str("\x1b[1m");
|
|
1255
|
-
result.push_str(part);
|
|
1256
|
-
result.push_str("\x1b[0m");
|
|
1257
|
-
} else {
|
|
1258
|
-
result.push_str(part);
|
|
1259
|
-
}
|
|
1260
|
-
is_bold = !is_bold;
|
|
1261
|
-
}
|
|
1262
|
-
result
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
fn format_line(line: &str) -> String {
|
|
1266
|
-
let mut formatted = line.to_string();
|
|
1267
|
-
let trimmed = line.trim_start();
|
|
1268
|
-
if (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
|
|
1269
|
-
&& trimmed.len() >= 2
|
|
1270
|
-
{
|
|
1271
|
-
let leading_len = line.len() - trimmed.len();
|
|
1272
|
-
let leading_spaces = &line[..leading_len];
|
|
1273
|
-
formatted = format!("{}•{}", leading_spaces, &trimmed[1..]);
|
|
1274
|
-
}
|
|
1275
|
-
process_inline_bold(&formatted)
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
fn unicode_width(s: &str) -> usize {
|
|
1279
|
-
let mut count = 0;
|
|
1280
|
-
for c in s.chars() {
|
|
1281
|
-
let val = c as u32;
|
|
1282
|
-
if (0x0E31..=0x0E31).contains(&val)
|
|
1283
|
-
|| (0x0E34..=0x0E3A).contains(&val)
|
|
1284
|
-
|| (0x0E47..=0x0E4E).contains(&val)
|
|
1285
|
-
{
|
|
1286
|
-
continue;
|
|
1287
|
-
}
|
|
1288
|
-
if is_cjk(c) {
|
|
1289
|
-
count += 2;
|
|
1290
|
-
} else {
|
|
1291
|
-
count += 1;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
count
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
fn is_cjk(c: char) -> bool {
|
|
1298
|
-
let val = c as u32;
|
|
1299
|
-
(0x4E00..=0x9FFF).contains(&val)
|
|
1300
|
-
|| (0x3400..=0x4DBF).contains(&val)
|
|
1301
|
-
|| (0x20000..=0x2A6DF).contains(&val)
|
|
1302
|
-
|| (0xAC00..=0xD7AF).contains(&val)
|
|
1303
|
-
|| (0x3040..=0x309F).contains(&val)
|
|
1304
|
-
|| (0x30A0..=0x30FF).contains(&val)
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
fn render_markdown_table(table_lines: &[String]) -> String {
|
|
1308
|
-
let mut rows: Vec<Vec<String>> = Vec::new();
|
|
1309
|
-
for line in table_lines {
|
|
1310
|
-
let trimmed = line.trim();
|
|
1311
|
-
if trimmed.is_empty() {
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1314
|
-
let content = &trimmed[1..trimmed.len() - 1];
|
|
1315
|
-
let content_escaped = content.replace("\\|", "\u{0000}");
|
|
1316
|
-
let cols: Vec<String> = content_escaped
|
|
1317
|
-
.split('|')
|
|
1318
|
-
.map(|s| s.replace("\u{0000}", "|").trim().to_string())
|
|
1319
|
-
.collect();
|
|
1320
|
-
rows.push(cols);
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
if rows.is_empty() {
|
|
1324
|
-
return String::new();
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
let mut has_separator = false;
|
|
1328
|
-
let mut separator_idx = None;
|
|
1329
|
-
for (i, row) in rows.iter().enumerate() {
|
|
1330
|
-
if row.iter().all(|col| {
|
|
1331
|
-
col.chars()
|
|
1332
|
-
.all(|c| c == '-' || c == ':' || c == ' ' || c == '\t')
|
|
1333
|
-
}) && !row.is_empty()
|
|
1334
|
-
{
|
|
1335
|
-
has_separator = true;
|
|
1336
|
-
separator_idx = Some(i);
|
|
1337
|
-
break;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
let mut data_rows: Vec<Vec<String>> = Vec::new();
|
|
1342
|
-
let mut header_row: Option<Vec<String>> = None;
|
|
1343
|
-
|
|
1344
|
-
if has_separator {
|
|
1345
|
-
let sep_idx = separator_idx.unwrap();
|
|
1346
|
-
if sep_idx > 0 {
|
|
1347
|
-
header_row = Some(rows[sep_idx - 1].clone());
|
|
1348
|
-
for (i, row) in rows.iter().enumerate() {
|
|
1349
|
-
if i != sep_idx && i != sep_idx - 1 {
|
|
1350
|
-
data_rows.push(row.clone());
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
} else {
|
|
1354
|
-
for (i, row) in rows.iter().enumerate() {
|
|
1355
|
-
if i != sep_idx {
|
|
1356
|
-
data_rows.push(row.clone());
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
} else {
|
|
1361
|
-
header_row = Some(rows[0].clone());
|
|
1362
|
-
data_rows = rows[1..].to_vec();
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
1366
|
-
let mut col_widths = vec![0; num_cols];
|
|
1367
|
-
|
|
1368
|
-
if let Some(ref header) = header_row {
|
|
1369
|
-
for (i, col) in header.iter().enumerate() {
|
|
1370
|
-
if i < num_cols {
|
|
1371
|
-
col_widths[i] = col_widths[i].max(unicode_width(col));
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
for row in &data_rows {
|
|
1376
|
-
for (i, col) in row.iter().enumerate() {
|
|
1377
|
-
if i < num_cols {
|
|
1378
|
-
col_widths[i] = col_widths[i].max(unicode_width(col));
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
let mut rendered = String::new();
|
|
1384
|
-
|
|
1385
|
-
let draw_border =
|
|
1386
|
-
|left: &str, mid: &str, right: &str, fill: &str, widths: &[usize]| -> String {
|
|
1387
|
-
let mut s = left.to_string();
|
|
1388
|
-
for (i, &w) in widths.iter().enumerate() {
|
|
1389
|
-
s.push_str(&fill.repeat(w + 2));
|
|
1390
|
-
if i < widths.len() - 1 {
|
|
1391
|
-
s.push_str(mid);
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
s.push_str(right);
|
|
1395
|
-
s.push('\n');
|
|
1396
|
-
s
|
|
1397
|
-
};
|
|
1398
|
-
|
|
1399
|
-
rendered.push_str(&draw_border("┌", "┬", "┐", "─", &col_widths));
|
|
1400
|
-
|
|
1401
|
-
if let Some(ref header) = header_row {
|
|
1402
|
-
rendered.push('│');
|
|
1403
|
-
for (i, col) in header.iter().enumerate() {
|
|
1404
|
-
let width = unicode_width(col);
|
|
1405
|
-
let padding = col_widths[i] - width;
|
|
1406
|
-
rendered.push_str(&format!(
|
|
1407
|
-
" \x1b[1;36m{}\x1b[0m{}",
|
|
1408
|
-
col,
|
|
1409
|
-
" ".repeat(padding + 1)
|
|
1410
|
-
));
|
|
1411
|
-
rendered.push('│');
|
|
1412
|
-
}
|
|
1413
|
-
rendered.push('\n');
|
|
1414
|
-
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
for (r_idx, row) in data_rows.iter().enumerate() {
|
|
1418
|
-
rendered.push('│');
|
|
1419
|
-
for i in 0..num_cols {
|
|
1420
|
-
let col_val = row.get(i).cloned().unwrap_or_default();
|
|
1421
|
-
let width = unicode_width(&col_val);
|
|
1422
|
-
let padding = col_widths[i] - width;
|
|
1423
|
-
rendered.push_str(&format!(" {}{}", col_val, " ".repeat(padding + 1)));
|
|
1424
|
-
rendered.push('│');
|
|
1425
|
-
}
|
|
1426
|
-
rendered.push('\n');
|
|
1427
|
-
|
|
1428
|
-
if r_idx < data_rows.len() - 1 {
|
|
1429
|
-
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
rendered.push_str(draw_border("└", "┴", "┘", "─", &col_widths).trim_end());
|
|
1434
|
-
rendered
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
struct CliStreamFormatter {
|
|
1438
|
-
line_buffer: String,
|
|
1439
|
-
table_buffer: Vec<String>,
|
|
1440
|
-
in_code_block: bool,
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
impl CliStreamFormatter {
|
|
1444
|
-
fn new() -> Self {
|
|
1445
|
-
Self {
|
|
1446
|
-
line_buffer: String::new(),
|
|
1447
|
-
table_buffer: Vec::new(),
|
|
1448
|
-
in_code_block: false,
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
fn process_char(&mut self, c: char, mut print_fn: impl FnMut(char)) {
|
|
1453
|
-
self.line_buffer.push(c);
|
|
1454
|
-
if c == '\n' {
|
|
1455
|
-
let mut line = self.line_buffer.clone();
|
|
1456
|
-
self.line_buffer.clear();
|
|
1457
|
-
if line.ends_with('\n') {
|
|
1458
|
-
line.pop();
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
let trimmed = line.trim();
|
|
1462
|
-
if trimmed.starts_with("```") {
|
|
1463
|
-
self.in_code_block = !self.in_code_block;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
if self.in_code_block {
|
|
1467
|
-
if !self.table_buffer.is_empty() {
|
|
1468
|
-
self.flush_table(&mut print_fn);
|
|
1469
|
-
}
|
|
1470
|
-
for ch in line.chars() {
|
|
1471
|
-
print_fn(ch);
|
|
1472
|
-
}
|
|
1473
|
-
print_fn('\n');
|
|
1474
|
-
for ch in " ".chars() {
|
|
1475
|
-
print_fn(ch);
|
|
1476
|
-
}
|
|
1477
|
-
} else {
|
|
1478
|
-
if is_table_line(&line) {
|
|
1479
|
-
self.table_buffer.push(line);
|
|
1480
|
-
} else {
|
|
1481
|
-
if !self.table_buffer.is_empty() {
|
|
1482
|
-
self.flush_table(&mut print_fn);
|
|
1483
|
-
}
|
|
1484
|
-
let formatted = format_line(&line);
|
|
1485
|
-
for ch in formatted.chars() {
|
|
1486
|
-
print_fn(ch);
|
|
1487
|
-
}
|
|
1488
|
-
print_fn('\n');
|
|
1489
|
-
for ch in " ".chars() {
|
|
1490
|
-
print_fn(ch);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
fn finalize(&mut self, mut print_fn: impl FnMut(char)) {
|
|
1498
|
-
if !self.line_buffer.is_empty() {
|
|
1499
|
-
let mut line = self.line_buffer.clone();
|
|
1500
|
-
self.line_buffer.clear();
|
|
1501
|
-
if line.ends_with('\n') {
|
|
1502
|
-
line.pop();
|
|
1503
|
-
}
|
|
1504
|
-
if self.in_code_block {
|
|
1505
|
-
if !self.table_buffer.is_empty() {
|
|
1506
|
-
self.flush_table(&mut print_fn);
|
|
1507
|
-
}
|
|
1508
|
-
for ch in line.chars() {
|
|
1509
|
-
print_fn(ch);
|
|
1510
|
-
}
|
|
1511
|
-
} else if is_table_line(&line) {
|
|
1512
|
-
self.table_buffer.push(line);
|
|
1513
|
-
} else {
|
|
1514
|
-
if !self.table_buffer.is_empty() {
|
|
1515
|
-
self.flush_table(&mut print_fn);
|
|
1516
|
-
}
|
|
1517
|
-
let formatted = format_line(&line);
|
|
1518
|
-
for ch in formatted.chars() {
|
|
1519
|
-
print_fn(ch);
|
|
1520
|
-
}
|
|
1464
|
+
println!("\n{MINT}✔ Mint Web is running!{RESET}\n");
|
|
1465
|
+
println!(" {BLUE}Web UI:{RESET} {MINT}http://localhost:9000{RESET}");
|
|
1466
|
+
if let Some(ip) = mint_core::api_server::get_local_ip() {
|
|
1467
|
+
println!(
|
|
1468
|
+
" {BLUE}Mobile:{RESET} {MINT}http://{}:9000{RESET}",
|
|
1469
|
+
ip
|
|
1470
|
+
);
|
|
1521
1471
|
}
|
|
1522
|
-
|
|
1523
|
-
if !self.table_buffer.is_empty() {
|
|
1524
|
-
self.flush_table(&mut print_fn);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1472
|
+
println!(" {BLUE}API Server:{RESET} {MINT}http://localhost:3000{RESET}\n");
|
|
1527
1473
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
print_fn(ch);
|
|
1532
|
-
}
|
|
1533
|
-
print_fn('\n');
|
|
1534
|
-
self.table_buffer.clear();
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
struct ActionStreamFilter {
|
|
1539
|
-
buffer: String,
|
|
1540
|
-
in_action: bool,
|
|
1541
|
-
action_text: String,
|
|
1542
|
-
actions: Vec<String>,
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
impl ActionStreamFilter {
|
|
1546
|
-
fn new() -> Self {
|
|
1547
|
-
Self {
|
|
1548
|
-
buffer: String::new(),
|
|
1549
|
-
in_action: false,
|
|
1550
|
-
action_text: String::new(),
|
|
1551
|
-
actions: Vec::new(),
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1474
|
+
println!("Point your browser to:");
|
|
1475
|
+
println!("{MINT}http://localhost:9000{RESET}\n");
|
|
1476
|
+
println!("{DIM}(API Requests & Error logs will be displayed below){RESET}\n");
|
|
1554
1477
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
if self.in_action {
|
|
1558
|
-
if c == ']' {
|
|
1559
|
-
self.in_action = false;
|
|
1560
|
-
self.actions.push(self.action_text.clone());
|
|
1561
|
-
self.action_text.clear();
|
|
1562
|
-
} else {
|
|
1563
|
-
self.action_text.push(c);
|
|
1564
|
-
}
|
|
1565
|
-
} else {
|
|
1566
|
-
self.buffer.push(c);
|
|
1567
|
-
let action_prefix = "[ACTION:";
|
|
1568
|
-
if action_prefix.starts_with(&self.buffer) {
|
|
1569
|
-
if self.buffer == action_prefix {
|
|
1570
|
-
self.in_action = true;
|
|
1571
|
-
self.buffer.clear();
|
|
1572
|
-
}
|
|
1573
|
-
} else {
|
|
1574
|
-
if let Some(pos) = self.buffer.find('[') {
|
|
1575
|
-
print_fn(&self.buffer[..pos]);
|
|
1576
|
-
let new_buf = self.buffer[pos..].to_string();
|
|
1577
|
-
self.buffer = new_buf;
|
|
1578
|
-
if !action_prefix.starts_with(&self.buffer) {
|
|
1579
|
-
print_fn(&self.buffer);
|
|
1580
|
-
self.buffer.clear();
|
|
1581
|
-
}
|
|
1582
|
-
} else {
|
|
1583
|
-
print_fn(&self.buffer);
|
|
1584
|
-
self.buffer.clear();
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1478
|
+
println!("{DIM}Press Ctrl+C to stop{RESET}\n");
|
|
1479
|
+
mint_core::start_api_server(3000).await?;
|
|
1588
1480
|
}
|
|
1481
|
+
_ => {}
|
|
1589
1482
|
}
|
|
1590
1483
|
|
|
1591
|
-
|
|
1592
|
-
if !self.buffer.is_empty() {
|
|
1593
|
-
print_fn(&self.buffer);
|
|
1594
|
-
self.buffer.clear();
|
|
1595
|
-
}
|
|
1596
|
-
if self.in_action {
|
|
1597
|
-
print_fn(&format!("[ACTION:{}", self.action_text));
|
|
1598
|
-
self.action_text.clear();
|
|
1599
|
-
self.in_action = false;
|
|
1600
|
-
}
|
|
1601
|
-
std::mem::take(&mut self.actions)
|
|
1602
|
-
}
|
|
1484
|
+
Ok(())
|
|
1603
1485
|
}
|
|
1604
1486
|
|
|
1605
1487
|
async fn run_cli_agent_task(task: Option<String>) -> Result<()> {
|
|
@@ -1638,2427 +1520,33 @@ async fn run_cli_agent_task(task: Option<String>) -> Result<()> {
|
|
|
1638
1520
|
}
|
|
1639
1521
|
}
|
|
1640
1522
|
|
|
1641
|
-
fn
|
|
1642
|
-
let
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
"open" => {
|
|
1648
|
-
println!("{WARN}System action:{RESET} opening {args}...\n");
|
|
1649
|
-
open_system_handler(args)?;
|
|
1650
|
-
}
|
|
1651
|
-
"open-app" => {
|
|
1652
|
-
println!("{WARN}System action:{RESET} launching app {args}...\n");
|
|
1653
|
-
launch_desktop_app(args)?;
|
|
1654
|
-
}
|
|
1655
|
-
"read-file" => {
|
|
1656
|
-
println!("{WARN}System action:{RESET} reading file {args}...\n");
|
|
1657
|
-
let path = PathBuf::from(args);
|
|
1658
|
-
read_file_content(&path)?;
|
|
1659
|
-
}
|
|
1660
|
-
"read-folder" => {
|
|
1661
|
-
println!("{WARN}System action:{RESET} reading folder {args}...\n");
|
|
1662
|
-
let path = PathBuf::from(args);
|
|
1663
|
-
read_folder_content(&path)?;
|
|
1664
|
-
}
|
|
1665
|
-
"run-shell" => {
|
|
1666
|
-
println!("{WARN}System action:{RESET} run local shell command");
|
|
1667
|
-
println!(" {args}");
|
|
1668
|
-
if confirm_shell_execution()? {
|
|
1669
|
-
let output = run_shell_command(args, &std::env::current_dir()?, true, config)?;
|
|
1670
|
-
print_shell_output(&output);
|
|
1671
|
-
} else {
|
|
1672
|
-
println!("Shell command cancelled.\n");
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
_ => {
|
|
1676
|
-
println!("{ERROR}Unknown system action:{RESET} {cmd} with args {args}\n");
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
} else {
|
|
1680
|
-
match trimmed {
|
|
1681
|
-
"read-folder" => {
|
|
1682
|
-
println!("{WARN}System action:{RESET} reading folder . ...\n");
|
|
1683
|
-
read_folder_content(&PathBuf::from("."))?;
|
|
1684
|
-
}
|
|
1685
|
-
_ => {
|
|
1686
|
-
println!("{ERROR}Invalid action format:{RESET} {trimmed}\n");
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
Ok(())
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
/// Per-session mutable state for the interactive chat loop.
|
|
1694
|
-
struct InteractiveSession {
|
|
1695
|
-
config: MintConfig,
|
|
1696
|
-
current_dir: PathBuf,
|
|
1697
|
-
fast_mode: bool,
|
|
1698
|
-
pending_image: Option<String>, // base64 data URI
|
|
1699
|
-
}
|
|
1523
|
+
async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
|
|
1524
|
+
let Some((owner, repo_name)) = parse_github_url(repo) else {
|
|
1525
|
+
anyhow::bail!(
|
|
1526
|
+
"Invalid GitHub repository URL/format. Please use 'owner/repo' or a full GitHub URL."
|
|
1527
|
+
);
|
|
1528
|
+
};
|
|
1700
1529
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
pasted_image: Option<String>,
|
|
1704
|
-
}
|
|
1530
|
+
use indicatif::{ProgressBar, ProgressStyle};
|
|
1531
|
+
use std::time::Duration;
|
|
1705
1532
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
/// Break out of the loop.
|
|
1713
|
-
Exit,
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
/// Route `/…` commands. Returns `None` if the input is not a slash command.
|
|
1717
|
-
async fn handle_slash_command(
|
|
1718
|
-
session: &mut InteractiveSession,
|
|
1719
|
-
query: &str,
|
|
1720
|
-
) -> Option<SlashResult> {
|
|
1721
|
-
let trimmed = query.trim();
|
|
1722
|
-
if !trimmed.starts_with('/') {
|
|
1723
|
-
return None;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
// Split into command word and optional rest
|
|
1727
|
-
let (cmd, rest) = trimmed
|
|
1728
|
-
.split_once(char::is_whitespace)
|
|
1729
|
-
.map(|(c, r)| (c, r.trim()))
|
|
1730
|
-
.unwrap_or((trimmed, ""));
|
|
1731
|
-
|
|
1732
|
-
match cmd {
|
|
1733
|
-
"/help" => {
|
|
1734
|
-
println!("\n{BLUE}────────────────────────────────────────────{RESET}");
|
|
1735
|
-
println!("{MINT} Mint Interactive Commands{RESET}");
|
|
1736
|
-
println!("{BLUE}────────────────────────────────────────────{RESET}");
|
|
1737
|
-
let commands = [
|
|
1738
|
-
("/help", "Show this help"),
|
|
1739
|
-
("/fast [on|off]", "Toggle fast mode (hide thinking traces)"),
|
|
1740
|
-
("/models [name]", "List providers or switch provider"),
|
|
1741
|
-
("/clear", "Clear conversation history"),
|
|
1742
|
-
("/cd <path>", "Change workspace directory"),
|
|
1743
|
-
("/image <path> [prompt]", "Attach image from file"),
|
|
1744
|
-
("/paste [prompt]", "Attach image from clipboard"),
|
|
1745
|
-
("Ctrl+V", "Paste clipboard image as [Image #1]"),
|
|
1746
|
-
("/learn <path>", "Import a persistent .md or .txt skill"),
|
|
1747
|
-
(
|
|
1748
|
-
"/skill add <path>",
|
|
1749
|
-
"Copy/install skill file or folder to global config",
|
|
1750
|
-
),
|
|
1751
|
-
(
|
|
1752
|
-
"/plugins <name> [prompt]",
|
|
1753
|
-
"Generate a skill.md file for a plugin/skill",
|
|
1754
|
-
),
|
|
1755
|
-
("/memory list", "Show recent interactions"),
|
|
1756
|
-
("/memory clear", "Clear all interactions"),
|
|
1757
|
-
("/memory get <key>", "Read a profile value"),
|
|
1758
|
-
("/memory set <key> <val>", "Store a profile value"),
|
|
1759
|
-
(
|
|
1760
|
-
"/image-provider [name]",
|
|
1761
|
-
"List image gen providers or switch default provider",
|
|
1762
|
-
),
|
|
1763
|
-
("/mcp list", "List configured MCP servers"),
|
|
1764
|
-
("/mcp allow <server> <tool>", "Allow an MCP tool"),
|
|
1765
|
-
("/stats", "Show session statistics"),
|
|
1766
|
-
("/exit", "Exit Mint"),
|
|
1767
|
-
("/code <task>", "Run in code-agent mode"),
|
|
1768
|
-
(
|
|
1769
|
-
"/multi-agent [on|off]",
|
|
1770
|
-
"Show or toggle Multi-Agent Collaboration system",
|
|
1771
|
-
),
|
|
1772
|
-
];
|
|
1773
|
-
for (cmd_name, desc) in &commands {
|
|
1774
|
-
println!(" {MINT}{:<30}{RESET} {DIM}{}{RESET}", cmd_name, desc);
|
|
1775
|
-
}
|
|
1776
|
-
println!();
|
|
1777
|
-
Some(SlashResult::Handled)
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
"/fast" => {
|
|
1781
|
-
session.fast_mode = match rest {
|
|
1782
|
-
"off" => false,
|
|
1783
|
-
"on" => true,
|
|
1784
|
-
"" => !session.fast_mode,
|
|
1785
|
-
_ => {
|
|
1786
|
-
println!("{WARN}/fast usage: /fast [on|off]{RESET}");
|
|
1787
|
-
return Some(SlashResult::Handled);
|
|
1788
|
-
}
|
|
1789
|
-
};
|
|
1790
|
-
if session.fast_mode {
|
|
1791
|
-
println!("{DIM}[Fast] mode ON — thinking traces hidden{RESET}\n");
|
|
1792
|
-
} else {
|
|
1793
|
-
println!("{DIM}[Fast] mode OFF{RESET}\n");
|
|
1794
|
-
}
|
|
1795
|
-
Some(SlashResult::Handled)
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
"/models" => {
|
|
1799
|
-
if rest.is_empty() {
|
|
1800
|
-
println!("\n{BLUE}Configured providers:{RESET}");
|
|
1801
|
-
for p in session.config.available_providers() {
|
|
1802
|
-
let active = if p == session.config.ai_provider.as_str() {
|
|
1803
|
-
format!(" {MINT}← active{RESET}")
|
|
1804
|
-
} else {
|
|
1805
|
-
String::new()
|
|
1806
|
-
};
|
|
1807
|
-
println!(" {p}{active}");
|
|
1808
|
-
}
|
|
1809
|
-
println!();
|
|
1810
|
-
} else {
|
|
1811
|
-
session.config.ai_provider = rest.to_owned();
|
|
1812
|
-
match mint_core::save_config(&session.config) {
|
|
1813
|
-
Ok(()) => {
|
|
1814
|
-
let active_model =
|
|
1815
|
-
active_model(&session.config.ai_provider, &session.config);
|
|
1816
|
-
let display_name = format!(
|
|
1817
|
-
"Changed model to {} • {}",
|
|
1818
|
-
session.config.ai_provider, active_model
|
|
1819
|
-
);
|
|
1820
|
-
|
|
1821
|
-
// Save system event interaction in memory
|
|
1822
|
-
if let Ok(memory) = MemoryStore::open_default() {
|
|
1823
|
-
let _ = memory.add_interaction_for_chat_with_fallback(
|
|
1824
|
-
CHAT_CLI_ID,
|
|
1825
|
-
&display_name,
|
|
1826
|
-
"",
|
|
1827
|
-
"system",
|
|
1828
|
-
"provider_change",
|
|
1829
|
-
None,
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
println!(
|
|
1834
|
-
"\n{DIM}───{RESET} {MINT}{}{RESET} {DIM}───{RESET}\n",
|
|
1835
|
-
display_name
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1838
|
-
Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
Some(SlashResult::Handled)
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
"/multi-agent" => {
|
|
1845
|
-
if rest == "on" || rest == "off" {
|
|
1846
|
-
session.config.enable_agent_collaboration = rest == "on";
|
|
1847
|
-
match mint_core::save_config(&session.config) {
|
|
1848
|
-
Ok(()) => println!(
|
|
1849
|
-
"{DIM}Multi-Agent collaboration set to: {}{RESET}\n",
|
|
1850
|
-
if session.config.enable_agent_collaboration {
|
|
1851
|
-
"Enabled"
|
|
1852
|
-
} else {
|
|
1853
|
-
"Disabled"
|
|
1854
|
-
}
|
|
1855
|
-
),
|
|
1856
|
-
Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
|
|
1857
|
-
}
|
|
1858
|
-
} else if !rest.is_empty() {
|
|
1859
|
-
println!("{WARN}Usage: /multi-agent [on|off]{RESET}\n");
|
|
1860
|
-
} else {
|
|
1861
|
-
println!("\n{BLUE}Multi-Agent System Settings:{RESET}");
|
|
1862
|
-
let collab_status = if session.config.enable_agent_collaboration {
|
|
1863
|
-
format!("{MINT}Enabled (on){RESET}")
|
|
1864
|
-
} else {
|
|
1865
|
-
format!("{DIM}Disabled (off){RESET}")
|
|
1866
|
-
};
|
|
1867
|
-
println!(" Global Collaboration: {collab_status}");
|
|
1868
|
-
println!("\n{BLUE}Configured Agents:{RESET}");
|
|
1869
|
-
if session.config.agents.is_empty() {
|
|
1870
|
-
println!(" No agents configured.");
|
|
1871
|
-
} else {
|
|
1872
|
-
for agent in &session.config.agents {
|
|
1873
|
-
let status_label = if agent.enabled {
|
|
1874
|
-
format!("{MINT}[Enabled]{RESET}")
|
|
1875
|
-
} else {
|
|
1876
|
-
format!("{DIM}[Disabled]{RESET}")
|
|
1877
|
-
};
|
|
1878
|
-
println!(
|
|
1879
|
-
" {:<15} {} Provider: {:<12} Model: {}",
|
|
1880
|
-
agent.name, status_label, agent.provider, agent.model
|
|
1881
|
-
);
|
|
1882
|
-
let truncated_instruction = if agent.system_instruction.len() > 60 {
|
|
1883
|
-
format!("{}...", &agent.system_instruction[..60].replace('\n', " "))
|
|
1884
|
-
} else {
|
|
1885
|
-
agent.system_instruction.replace('\n', " ")
|
|
1886
|
-
};
|
|
1887
|
-
println!(" {DIM}Instruction: {truncated_instruction}{RESET}");
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
println!();
|
|
1891
|
-
}
|
|
1892
|
-
Some(SlashResult::Handled)
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
"/image-provider" => {
|
|
1896
|
-
let mut available = Vec::new();
|
|
1897
|
-
if !session.config.api_key.trim().is_empty() {
|
|
1898
|
-
available.push("nanobanana");
|
|
1899
|
-
}
|
|
1900
|
-
if !session.config.openai_api_key.trim().is_empty() {
|
|
1901
|
-
available.push("dalle");
|
|
1902
|
-
}
|
|
1903
|
-
if !session.config.stability_api_key.trim().is_empty() {
|
|
1904
|
-
available.push("stability");
|
|
1905
|
-
}
|
|
1906
|
-
if !session.config.ideogram_api_key.trim().is_empty() {
|
|
1907
|
-
available.push("ideogram");
|
|
1908
|
-
}
|
|
1909
|
-
if !session.config.replicate_api_key.trim().is_empty() {
|
|
1910
|
-
available.push("replicate");
|
|
1911
|
-
}
|
|
1912
|
-
if available.is_empty() {
|
|
1913
|
-
available.push("nanobanana");
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
if rest.is_empty() {
|
|
1917
|
-
println!("\n{BLUE}Configured image generation providers:{RESET}");
|
|
1918
|
-
for p in &available {
|
|
1919
|
-
let active = if p == &session.config.image_gen_provider.as_str() {
|
|
1920
|
-
format!(" {MINT}← active{RESET}")
|
|
1921
|
-
} else {
|
|
1922
|
-
String::new()
|
|
1923
|
-
};
|
|
1924
|
-
println!(" {p}{active}");
|
|
1925
|
-
}
|
|
1926
|
-
println!();
|
|
1927
|
-
} else {
|
|
1928
|
-
if available.contains(&rest) {
|
|
1929
|
-
session.config.image_gen_provider = rest.to_owned();
|
|
1930
|
-
match mint_core::save_config(&session.config) {
|
|
1931
|
-
Ok(()) => println!(
|
|
1932
|
-
"{DIM}Switched default image provider to: {}{RESET}\n",
|
|
1933
|
-
session.config.image_gen_provider
|
|
1934
|
-
),
|
|
1935
|
-
Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
|
|
1936
|
-
}
|
|
1937
|
-
} else {
|
|
1938
|
-
println!("{ERROR}Provider '{rest}' is not configured or invalid.{RESET}\n");
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
Some(SlashResult::Handled)
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
"/clear" | "/reset" => {
|
|
1945
|
-
println!("Clear conversation history? [y/N] ");
|
|
1946
|
-
if let Ok(true) = confirm("Clear conversation history? [y/N] ") {
|
|
1947
|
-
if let Ok(memory) = MemoryStore::open_default() {
|
|
1948
|
-
match memory.clear_interactions() {
|
|
1949
|
-
Ok(count) => println!("{DIM}Cleared {count} interactions.{RESET}"),
|
|
1950
|
-
Err(error) => println!("{ERROR}Memory error:{RESET} {error}"),
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
println!("{DIM}Conversation context cleared.{RESET}\n");
|
|
1954
|
-
}
|
|
1955
|
-
Some(SlashResult::Handled)
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
"/cd" => {
|
|
1959
|
-
if rest.is_empty() {
|
|
1960
|
-
println!("{WARN}/cd requires a path{RESET}\n");
|
|
1961
|
-
} else {
|
|
1962
|
-
let new_dir = PathBuf::from(rest);
|
|
1963
|
-
if new_dir.is_dir() {
|
|
1964
|
-
session.current_dir = new_dir.canonicalize().unwrap_or(new_dir);
|
|
1965
|
-
println!(
|
|
1966
|
-
"{DIM}Workspace: {}{RESET}\n",
|
|
1967
|
-
format_path_with_tilde(&session.current_dir)
|
|
1968
|
-
);
|
|
1969
|
-
} else {
|
|
1970
|
-
println!("{ERROR}Directory not found:{RESET} {rest}\n");
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
Some(SlashResult::Handled)
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
"/image" => {
|
|
1977
|
-
let (img_path, prompt) = rest
|
|
1978
|
-
.split_once(char::is_whitespace)
|
|
1979
|
-
.map(|(p, r)| (p, r.trim()))
|
|
1980
|
-
.unwrap_or((rest, ""));
|
|
1981
|
-
|
|
1982
|
-
if img_path.is_empty() {
|
|
1983
|
-
println!("{WARN}/image usage: /image <path> [prompt]{RESET}\n");
|
|
1984
|
-
return Some(SlashResult::Handled);
|
|
1985
|
-
}
|
|
1986
|
-
match image::load_image_as_data_uri(std::path::Path::new(img_path)) {
|
|
1987
|
-
Ok(uri) => {
|
|
1988
|
-
if let Some(ref mut current) = session.pending_image {
|
|
1989
|
-
current.push(' ');
|
|
1990
|
-
current.push_str(&uri);
|
|
1991
|
-
} else {
|
|
1992
|
-
session.pending_image = Some(uri);
|
|
1993
|
-
}
|
|
1994
|
-
if prompt.is_empty() {
|
|
1995
|
-
println!("{DIM}Image attached — type your prompt and press Enter{RESET}\n");
|
|
1996
|
-
Some(SlashResult::Handled)
|
|
1997
|
-
} else {
|
|
1998
|
-
Some(SlashResult::ForwardToAgent(prompt.to_owned()))
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
Err(e) => {
|
|
2002
|
-
println!("{ERROR}Failed to load image:{RESET} {e}\n");
|
|
2003
|
-
Some(SlashResult::Handled)
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
"/paste" => match image::read_clipboard_image() {
|
|
2009
|
-
Ok(Some(uri)) => {
|
|
2010
|
-
if let Some(ref mut current) = session.pending_image {
|
|
2011
|
-
current.push(' ');
|
|
2012
|
-
current.push_str(&uri);
|
|
2013
|
-
} else {
|
|
2014
|
-
session.pending_image = Some(uri);
|
|
2015
|
-
}
|
|
2016
|
-
if rest.is_empty() {
|
|
2017
|
-
println!(
|
|
2018
|
-
"{DIM}Clipboard image attached — type your prompt and press Enter{RESET}\n"
|
|
2019
|
-
);
|
|
2020
|
-
Some(SlashResult::Handled)
|
|
2021
|
-
} else {
|
|
2022
|
-
Some(SlashResult::ForwardToAgent(rest.to_owned()))
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
Ok(None) => {
|
|
2026
|
-
println!("{WARN}No image found in clipboard.{RESET}\n");
|
|
2027
|
-
Some(SlashResult::Handled)
|
|
2028
|
-
}
|
|
2029
|
-
Err(e) => {
|
|
2030
|
-
println!("{ERROR}Clipboard error:{RESET} {e}\n");
|
|
2031
|
-
Some(SlashResult::Handled)
|
|
2032
|
-
}
|
|
2033
|
-
},
|
|
2034
|
-
|
|
2035
|
-
"/learn" => {
|
|
2036
|
-
if rest.is_empty() {
|
|
2037
|
-
let mut skills = match MemoryStore::open_default() {
|
|
2038
|
-
Ok(m) => match m.learned_skills(100) {
|
|
2039
|
-
Ok(s) => s,
|
|
2040
|
-
Err(e) => {
|
|
2041
|
-
println!("{ERROR}Error loading custom skills: {e}{RESET}\n");
|
|
2042
|
-
return Some(SlashResult::Handled);
|
|
2043
|
-
}
|
|
2044
|
-
},
|
|
2045
|
-
Err(e) => {
|
|
2046
|
-
println!("{ERROR}Memory error: {e}{RESET}\n");
|
|
2047
|
-
return Some(SlashResult::Handled);
|
|
2048
|
-
}
|
|
2049
|
-
};
|
|
2050
|
-
|
|
2051
|
-
if let Some(home) = dirs::home_dir() {
|
|
2052
|
-
let global_skills_path = home.join(".config").join("mint").join("mint-skills");
|
|
2053
|
-
mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
|
|
2054
|
-
}
|
|
2055
|
-
let workspace_skills_path1 = session.current_dir.join(".agents").join("skills");
|
|
2056
|
-
mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
|
|
2057
|
-
let workspace_skills_path2 = session.current_dir.join("skills");
|
|
2058
|
-
mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
|
|
2059
|
-
|
|
2060
|
-
let mut unique_skills = std::collections::BTreeMap::new();
|
|
2061
|
-
for skill in skills {
|
|
2062
|
-
let loc = if skill.source_path.contains("/.config/mint/mint-skills") {
|
|
2063
|
-
"Global"
|
|
2064
|
-
} else if skill.source_path.contains("/skills")
|
|
2065
|
-
|| skill.source_path.contains("/.agents/skills")
|
|
2066
|
-
{
|
|
2067
|
-
"Workspace"
|
|
2068
|
-
} else {
|
|
2069
|
-
"Taught"
|
|
2070
|
-
};
|
|
2071
|
-
unique_skills.insert(skill.name.clone(), (skill, loc));
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
if unique_skills.is_empty() {
|
|
2075
|
-
println!("No learned skills found. Use '/learn <path>' to add one.\n");
|
|
2076
|
-
} else {
|
|
2077
|
-
println!("Learned AI Skills:");
|
|
2078
|
-
for (name, (skill, loc)) in &unique_skills {
|
|
2079
|
-
if *loc == "Taught" {
|
|
2080
|
-
println!(" ● [{}] {}", loc, name);
|
|
2081
|
-
} else {
|
|
2082
|
-
println!(" ● [{}] {} (Source: {})", loc, name, skill.source_path);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
println!();
|
|
2086
|
-
}
|
|
2087
|
-
} else {
|
|
2088
|
-
let path = PathBuf::from(rest);
|
|
2089
|
-
let path = if path.is_absolute() {
|
|
2090
|
-
path
|
|
2091
|
-
} else {
|
|
2092
|
-
session.current_dir.join(path)
|
|
2093
|
-
};
|
|
2094
|
-
match skills::learn(&path) {
|
|
2095
|
-
Ok(skill) => println!(
|
|
2096
|
-
"{DIM}Learned skill: {} ({}){RESET}\n",
|
|
2097
|
-
skill.name, skill.source_path
|
|
2098
|
-
),
|
|
2099
|
-
Err(error) => println!("{ERROR}Learn error:{RESET} {error}\n"),
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
Some(SlashResult::Handled)
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
"/skill" => {
|
|
2106
|
-
let (subcmd, args) = rest
|
|
2107
|
-
.split_once(char::is_whitespace)
|
|
2108
|
-
.map(|(c, a)| (c, a.trim()))
|
|
2109
|
-
.unwrap_or((rest, ""));
|
|
2110
|
-
|
|
2111
|
-
match subcmd {
|
|
2112
|
-
"add" | "install" => {
|
|
2113
|
-
if args.is_empty() {
|
|
2114
|
-
println!(
|
|
2115
|
-
"{WARN}/skill add <path> requires a path to a skill file or folder{RESET}\n"
|
|
2116
|
-
);
|
|
2117
|
-
} else {
|
|
2118
|
-
let source_path = PathBuf::from(args);
|
|
2119
|
-
let source_path = if source_path.is_absolute() {
|
|
2120
|
-
source_path
|
|
2121
|
-
} else {
|
|
2122
|
-
session.current_dir.join(source_path)
|
|
2123
|
-
};
|
|
2124
|
-
|
|
2125
|
-
if !source_path.exists() {
|
|
2126
|
-
println!("{ERROR}Source skill path not found: {}{RESET}\n", args);
|
|
2127
|
-
} else if let Some(home) = dirs::home_dir() {
|
|
2128
|
-
let global_skills_path =
|
|
2129
|
-
home.join(".config").join("mint").join("mint-skills");
|
|
2130
|
-
if !global_skills_path.exists() {
|
|
2131
|
-
let _ = std::fs::create_dir_all(&global_skills_path);
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
let name = source_path
|
|
2135
|
-
.file_name()
|
|
2136
|
-
.and_then(|n| n.to_str())
|
|
2137
|
-
.unwrap_or("skill");
|
|
2138
|
-
|
|
2139
|
-
let dest_path = global_skills_path.join(name);
|
|
2140
|
-
let copy_res = if source_path.is_dir() {
|
|
2141
|
-
copy_dir_all(&source_path, &dest_path)
|
|
2142
|
-
} else {
|
|
2143
|
-
std::fs::copy(&source_path, &dest_path)
|
|
2144
|
-
.map(|_| ())
|
|
2145
|
-
.map_err(|e| e)
|
|
2146
|
-
};
|
|
2147
|
-
|
|
2148
|
-
match copy_res {
|
|
2149
|
-
Ok(()) => println!(
|
|
2150
|
-
"{DIM}Skill successfully copied to Global config: {}{RESET}\n",
|
|
2151
|
-
dest_path.display()
|
|
2152
|
-
),
|
|
2153
|
-
Err(e) => println!(
|
|
2154
|
-
"{ERROR}Failed to copy skill to Global: {}{RESET}\n",
|
|
2155
|
-
e
|
|
2156
|
-
),
|
|
2157
|
-
}
|
|
2158
|
-
} else {
|
|
2159
|
-
println!(
|
|
2160
|
-
"{ERROR}Unable to resolve home directory for Global config.{RESET}\n"
|
|
2161
|
-
);
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
_ => {
|
|
2166
|
-
println!("{WARN}Usage: /skill add <path> (or /skill install <path>){RESET}\n");
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
Some(SlashResult::Handled)
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
"/memory" => {
|
|
2173
|
-
let memory = match MemoryStore::open_default() {
|
|
2174
|
-
Ok(m) => m,
|
|
2175
|
-
Err(e) => {
|
|
2176
|
-
println!("{ERROR}Memory error:{RESET} {e}\n");
|
|
2177
|
-
return Some(SlashResult::Handled);
|
|
2178
|
-
}
|
|
2179
|
-
};
|
|
2180
|
-
let (subcmd, args) = rest
|
|
2181
|
-
.split_once(char::is_whitespace)
|
|
2182
|
-
.map(|(c, a)| (c, a.trim()))
|
|
2183
|
-
.unwrap_or((rest, ""));
|
|
2184
|
-
match subcmd {
|
|
2185
|
-
"list" | "" => match memory.recent_interactions_for_chat(CHAT_CLI_ID, 10) {
|
|
2186
|
-
Ok(items) => {
|
|
2187
|
-
if items.is_empty() {
|
|
2188
|
-
println!("{DIM}No interactions yet.{RESET}\n");
|
|
2189
|
-
} else {
|
|
2190
|
-
println!("\n{BLUE}Recent interactions:{RESET}");
|
|
2191
|
-
for item in items.iter().rev() {
|
|
2192
|
-
println!(
|
|
2193
|
-
" {DIM}[{}]{RESET} {BLUE}You:{RESET} {}",
|
|
2194
|
-
&item.created_at[..16.min(item.created_at.len())],
|
|
2195
|
-
if item.user_text.chars().count() > 80 {
|
|
2196
|
-
let short: String =
|
|
2197
|
-
item.user_text.chars().take(80).collect();
|
|
2198
|
-
format!("{}…", short)
|
|
2199
|
-
} else {
|
|
2200
|
-
item.user_text.clone()
|
|
2201
|
-
}
|
|
2202
|
-
);
|
|
2203
|
-
}
|
|
2204
|
-
println!();
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
|
|
2208
|
-
},
|
|
2209
|
-
"get" => {
|
|
2210
|
-
if args.is_empty() {
|
|
2211
|
-
println!("{WARN}/memory get <key>{RESET}\n");
|
|
2212
|
-
} else {
|
|
2213
|
-
match memory.get_profile(args) {
|
|
2214
|
-
Ok(Some(val)) => println!("{val}\n"),
|
|
2215
|
-
Ok(None) => println!("{DIM}(not set){RESET}\n"),
|
|
2216
|
-
Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
"set" => {
|
|
2221
|
-
let (key, val) = args
|
|
2222
|
-
.split_once(char::is_whitespace)
|
|
2223
|
-
.map(|(k, v)| (k, v.trim()))
|
|
2224
|
-
.unwrap_or((args, ""));
|
|
2225
|
-
if key.is_empty() {
|
|
2226
|
-
println!("{WARN}/memory set <key> <value>{RESET}\n");
|
|
2227
|
-
} else {
|
|
2228
|
-
match memory.set_profile(key, val) {
|
|
2229
|
-
Ok(()) => println!("{DIM}Stored {key}.{RESET}\n"),
|
|
2230
|
-
Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
"skills" => match memory.learned_skills(20) {
|
|
2235
|
-
Ok(skills) => {
|
|
2236
|
-
if skills.is_empty() {
|
|
2237
|
-
println!("{DIM}No learned skills.{RESET}\n");
|
|
2238
|
-
} else {
|
|
2239
|
-
println!("\n{BLUE}Learned skills:{RESET}");
|
|
2240
|
-
for s in &skills {
|
|
2241
|
-
println!(" [{}] {} — {}", s.id, s.name, s.source_path);
|
|
2242
|
-
}
|
|
2243
|
-
println!();
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
|
|
2247
|
-
},
|
|
2248
|
-
"clear" => match memory.clear_interactions_for_chat(CHAT_CLI_ID) {
|
|
2249
|
-
Ok(count) => println!("{DIM}Cleared {count} interactions.{RESET}\n"),
|
|
2250
|
-
Err(e) => println!("{ERROR}Error:{RESET} {e}\n"),
|
|
2251
|
-
},
|
|
2252
|
-
_ => println!(
|
|
2253
|
-
"{WARN}/memory usage: list | clear | get <key> | set <key> <val> | skills{RESET}\n"
|
|
2254
|
-
),
|
|
2255
|
-
}
|
|
2256
|
-
Some(SlashResult::Handled)
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
"/mcp" => {
|
|
2260
|
-
let (subcmd, args) = rest
|
|
2261
|
-
.split_once(char::is_whitespace)
|
|
2262
|
-
.map(|(c, a)| (c, a.trim()))
|
|
2263
|
-
.unwrap_or((rest, ""));
|
|
2264
|
-
match subcmd {
|
|
2265
|
-
"list" | "" => match mcp::list() {
|
|
2266
|
-
Ok(servers) => {
|
|
2267
|
-
println!("\n{BLUE}MCP servers:{RESET}");
|
|
2268
|
-
print_mcp_servers(&servers);
|
|
2269
|
-
}
|
|
2270
|
-
Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
|
|
2271
|
-
},
|
|
2272
|
-
"allow" => {
|
|
2273
|
-
let mut parts = args.split_whitespace();
|
|
2274
|
-
let server = parts.next();
|
|
2275
|
-
let tool = parts.next();
|
|
2276
|
-
if let (Some(server), Some(tool)) = (server, tool) {
|
|
2277
|
-
match mcp::allow(server, tool) {
|
|
2278
|
-
Ok(true) => println!("{DIM}Allowed MCP tool: {server}/{tool}{RESET}\n"),
|
|
2279
|
-
Ok(false) => {
|
|
2280
|
-
println!("{DIM}MCP tool already allowed: {server}/{tool}{RESET}\n")
|
|
2281
|
-
}
|
|
2282
|
-
Err(e) => println!("{ERROR}MCP error:{RESET} {e}\n"),
|
|
2283
|
-
}
|
|
2284
|
-
} else {
|
|
2285
|
-
println!("{WARN}/mcp allow usage: <server> <tool>{RESET}\n");
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
_ => println!("{WARN}/mcp usage: list | allow <server> <tool>{RESET}\n"),
|
|
2289
|
-
}
|
|
2290
|
-
Some(SlashResult::Handled)
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
"/stats" => {
|
|
2294
|
-
let provider = &session.config.ai_provider;
|
|
2295
|
-
let model = active_model(provider, &session.config);
|
|
2296
|
-
let interactions = MemoryStore::open_default()
|
|
2297
|
-
.and_then(|m| m.recent_interactions_for_chat(CHAT_CLI_ID, 1000))
|
|
2298
|
-
.map(|v| v.len())
|
|
2299
|
-
.unwrap_or(0);
|
|
2300
|
-
println!("\n{BLUE}─ Session Stats ─────────────────────────{RESET}");
|
|
2301
|
-
println!(" Provider : {MINT}{provider}{RESET}");
|
|
2302
|
-
println!(" Model : {model}");
|
|
2303
|
-
println!(
|
|
2304
|
-
" Workspace: {}",
|
|
2305
|
-
format_path_with_tilde(&session.current_dir)
|
|
2306
|
-
);
|
|
2307
|
-
println!(
|
|
2308
|
-
" Fast mode: {}",
|
|
2309
|
-
if session.fast_mode { "on" } else { "off" }
|
|
2310
|
-
);
|
|
2311
|
-
println!(" Memory : {interactions} interactions");
|
|
2312
|
-
if let Some(ref img_data) = session.pending_image {
|
|
2313
|
-
let count = img_data.split_whitespace().count();
|
|
2314
|
-
if count > 1 {
|
|
2315
|
-
println!(" Images : {WARN}{} images attached{RESET}", count);
|
|
2316
|
-
} else {
|
|
2317
|
-
println!(" Image : {WARN}attached{RESET}");
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
println!();
|
|
2321
|
-
Some(SlashResult::Handled)
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
"/exit" | "/quit" => Some(SlashResult::Exit),
|
|
2325
|
-
|
|
2326
|
-
"/plugins" | "/plugin" => {
|
|
2327
|
-
let (plugin_name, prompt) = rest
|
|
2328
|
-
.split_once(char::is_whitespace)
|
|
2329
|
-
.map(|(p, r)| (p, r.trim()))
|
|
2330
|
-
.unwrap_or((rest, ""));
|
|
2331
|
-
|
|
2332
|
-
if plugin_name.is_empty() {
|
|
2333
|
-
println!("\n{BLUE}────────────────────────────────────────────{RESET}");
|
|
2334
|
-
println!("{MINT} Available Plugins & Skills{RESET}");
|
|
2335
|
-
println!("{BLUE}────────────────────────────────────────────{RESET}");
|
|
2336
|
-
|
|
2337
|
-
println!("{MINT}Native Plugins:{RESET}");
|
|
2338
|
-
for p in native_plugins() {
|
|
2339
|
-
println!(" - {BLUE}{:<20}{RESET} {}", p.name, p.description);
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
println!("\n{MINT}Custom Skills/Plugins (Active):{RESET}");
|
|
2343
|
-
let mut active_skills = std::collections::HashSet::new();
|
|
2344
|
-
if let Ok(memory) = MemoryStore::open_default() {
|
|
2345
|
-
match memory.learned_skills(100) {
|
|
2346
|
-
Ok(skills) => {
|
|
2347
|
-
if skills.is_empty() {
|
|
2348
|
-
println!(
|
|
2349
|
-
" {DIM}(No custom skills/plugins learned yet. Use '/learn <path>' to add one.){RESET}"
|
|
2350
|
-
);
|
|
2351
|
-
} else {
|
|
2352
|
-
for s in &skills {
|
|
2353
|
-
println!(" - {BLUE}{:<20}{RESET} {}", s.name, s.source_path);
|
|
2354
|
-
|
|
2355
|
-
// Try to store canonicalized active path
|
|
2356
|
-
if let Ok(canon) = Path::new(&s.source_path).canonicalize() {
|
|
2357
|
-
active_skills.insert(canon);
|
|
2358
|
-
} else {
|
|
2359
|
-
active_skills.insert(PathBuf::from(&s.source_path));
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
Err(e) => println!(" {ERROR}Error loading custom skills: {e}{RESET}"),
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
// Scan local workspace skills directory
|
|
2369
|
-
let local_skills_dir = session.current_dir.join("skills");
|
|
2370
|
-
if local_skills_dir.is_dir()
|
|
2371
|
-
&& let Ok(entries) = fs::read_dir(&local_skills_dir)
|
|
2372
|
-
{
|
|
2373
|
-
let mut local_files = Vec::new();
|
|
2374
|
-
for entry in entries.flatten() {
|
|
2375
|
-
let path = entry.path();
|
|
2376
|
-
if path.is_file()
|
|
2377
|
-
&& let Some(ext) = path.extension().and_then(|e| e.to_str())
|
|
2378
|
-
&& matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt")
|
|
2379
|
-
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
|
|
2380
|
-
{
|
|
2381
|
-
if let Ok(canon_path) = path.canonicalize() {
|
|
2382
|
-
local_files.push((name.to_owned(), canon_path));
|
|
2383
|
-
} else {
|
|
2384
|
-
local_files.push((name.to_owned(), path));
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
let inactive_files: Vec<_> = local_files
|
|
2390
|
-
.into_iter()
|
|
2391
|
-
.filter(|(_, full_path)| !active_skills.contains(full_path))
|
|
2392
|
-
.map(|(name, _)| name)
|
|
2393
|
-
.collect();
|
|
2394
|
-
|
|
2395
|
-
if !inactive_files.is_empty() {
|
|
2396
|
-
println!(
|
|
2397
|
-
"\n{MINT}Local Skill Files (Found but not learned/active yet):{RESET}"
|
|
2398
|
-
);
|
|
2399
|
-
for file in inactive_files {
|
|
2400
|
-
println!(
|
|
2401
|
-
" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
|
|
2402
|
-
file, file, file
|
|
2403
|
-
);
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
println!();
|
|
2409
|
-
Some(SlashResult::Handled)
|
|
2410
|
-
} else {
|
|
2411
|
-
let agent_prompt = if prompt.is_empty() {
|
|
2412
|
-
format!(
|
|
2413
|
-
"Create a skill markdown file for the plugin '{}' at the path 'skills/{}.md' in the workspace. Write the appropriate instructions inside.",
|
|
2414
|
-
plugin_name, plugin_name
|
|
2415
|
-
)
|
|
2416
|
-
} else {
|
|
2417
|
-
format!(
|
|
2418
|
-
"Create a skill markdown file for the plugin '{}' at the path 'skills/{}.md' in the workspace based on these instructions: {}",
|
|
2419
|
-
plugin_name, plugin_name, prompt
|
|
2420
|
-
)
|
|
2421
|
-
};
|
|
2422
|
-
Some(SlashResult::ForwardToAgent(agent_prompt))
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
"/code" => {
|
|
2427
|
-
if rest.is_empty() {
|
|
2428
|
-
println!("{WARN}/code requires a task description{RESET}\n");
|
|
2429
|
-
Some(SlashResult::Handled)
|
|
2430
|
-
} else {
|
|
2431
|
-
Some(SlashResult::ForwardToAgent(format!("[code] {rest}")))
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
_ => {
|
|
2436
|
-
// Unknown slash command — treat as normal message to the agent
|
|
2437
|
-
None
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
async fn run_interactive_chat() -> Result<()> {
|
|
2443
|
-
let config = load_config()?;
|
|
2444
|
-
|
|
2445
|
-
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
2446
|
-
|
|
2447
|
-
let provider = &config.ai_provider.clone();
|
|
2448
|
-
let model = active_model(provider, &config).to_owned();
|
|
2449
|
-
|
|
2450
|
-
// Print startup banner
|
|
2451
|
-
let now = chrono::Local::now();
|
|
2452
|
-
let year = now.format("%Y").to_string().parse::<i32>().unwrap_or(2026) + 543;
|
|
2453
|
-
let date_time = format!(
|
|
2454
|
-
"{}/{:02}/{:02} {:02}:{:02}",
|
|
2455
|
-
now.format("%d"),
|
|
2456
|
-
now.format("%m"),
|
|
2457
|
-
year,
|
|
2458
|
-
now.format("%H"),
|
|
2459
|
-
now.format("%M")
|
|
2460
|
-
);
|
|
2461
|
-
let version = env!("CARGO_PKG_VERSION");
|
|
2462
|
-
let clean_provider_name = format_provider_display_name(provider, &config);
|
|
2463
|
-
let line1_text = format!("[Mint] v{} | Active AI: {}", version, clean_provider_name);
|
|
2464
|
-
let line2_text = format!("{} • {}", date_time, model);
|
|
2465
|
-
|
|
2466
|
-
let len1 = line1_text.chars().count();
|
|
2467
|
-
let len2 = line2_text.chars().count();
|
|
2468
|
-
let content_width = std::cmp::max(len1, len2);
|
|
2469
|
-
let border_len = content_width + 2;
|
|
2470
|
-
|
|
2471
|
-
let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
|
|
2472
|
-
let term_width = term_width as usize;
|
|
2473
|
-
let ascii_width = 34;
|
|
2474
|
-
let spacing = 3;
|
|
2475
|
-
let box_width = border_len + 2;
|
|
2476
|
-
|
|
2477
|
-
if term_width >= ascii_width + spacing + box_width {
|
|
2478
|
-
println!(
|
|
2479
|
-
"{MINT} __ __ _ _ ___ _ ___ {RESET} {DIM}╭{}╮{RESET}",
|
|
2480
|
-
"─".repeat(border_len)
|
|
2481
|
-
);
|
|
2482
|
-
println!(
|
|
2483
|
-
"{MINT}| \\/ (_)_ __ | |_ / __| | |_ _|{RESET} {DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
|
|
2484
|
-
version,
|
|
2485
|
-
clean_provider_name,
|
|
2486
|
-
" ".repeat(content_width - len1)
|
|
2487
|
-
);
|
|
2488
|
-
println!(
|
|
2489
|
-
"{MINT}| |\\/| | | '_ \\| _| (__| |__ | | {RESET} {DIM}│{RESET} {DIM}{}{}{RESET} {DIM}│{RESET}",
|
|
2490
|
-
line2_text,
|
|
2491
|
-
" ".repeat(content_width - len2)
|
|
2492
|
-
);
|
|
2493
|
-
println!(
|
|
2494
|
-
"{MINT}|_| |_|_|_| |_|\\__|\\___|\\___|___|{RESET} {DIM}╰{}╯{RESET}",
|
|
2495
|
-
"─".repeat(border_len)
|
|
2496
|
-
);
|
|
2497
|
-
} else {
|
|
2498
|
-
println!("{DIM}╭{}╮{RESET}", "─".repeat(border_len));
|
|
2499
|
-
println!(
|
|
2500
|
-
"{DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
|
|
2501
|
-
version,
|
|
2502
|
-
clean_provider_name,
|
|
2503
|
-
" ".repeat(content_width - len1)
|
|
2504
|
-
);
|
|
2505
|
-
println!(
|
|
2506
|
-
"{DIM}│{RESET} {DIM}{}{}{RESET} {DIM}│{RESET}",
|
|
2507
|
-
line2_text,
|
|
2508
|
-
" ".repeat(content_width - len2)
|
|
2509
|
-
);
|
|
2510
|
-
println!("{DIM}╰{}╯{RESET}", "─".repeat(border_len));
|
|
2511
|
-
println!("{MINT} __ __ _ _ ___ _ ___ {RESET}");
|
|
2512
|
-
println!("{MINT}| \\/ (_)_ __ | |_ / __| | |_ _|{RESET}");
|
|
2513
|
-
println!("{MINT}| |\\/| | | '_ \\| _| (__| |__ | | {RESET}");
|
|
2514
|
-
println!("{MINT}|_| |_|_|_| |_|\\__|\\___|\\___|___|{RESET}");
|
|
2515
|
-
}
|
|
2516
|
-
println!("Type naturally or /help for commands. Ctrl+V pastes images. Ctrl+D exits.\n");
|
|
2517
|
-
|
|
2518
|
-
let mut session = InteractiveSession {
|
|
2519
|
-
config,
|
|
2520
|
-
current_dir: current_dir.clone(),
|
|
2521
|
-
fast_mode: false,
|
|
2522
|
-
pending_image: None,
|
|
2523
|
-
};
|
|
2524
|
-
|
|
2525
|
-
let mut printed_update = false;
|
|
2526
|
-
if let Some((current, latest)) = updater::get_cached_update_notice() {
|
|
2527
|
-
updater::print_update_notice(¤t, &latest);
|
|
2528
|
-
printed_update = true;
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
let mut update_handle = if updater::should_check_for_update() {
|
|
2532
|
-
Some(tokio::task::spawn_blocking(
|
|
2533
|
-
updater::check_for_update_quietly,
|
|
2534
|
-
))
|
|
2535
|
-
} else {
|
|
2536
|
-
None
|
|
2537
|
-
};
|
|
2538
|
-
|
|
2539
|
-
loop {
|
|
2540
|
-
if let Some(handle) = update_handle.take() {
|
|
2541
|
-
if handle.is_finished() {
|
|
2542
|
-
if let Ok(Some((current, latest))) = handle.await
|
|
2543
|
-
&& !printed_update
|
|
2544
|
-
{
|
|
2545
|
-
updater::print_update_notice(¤t, &latest);
|
|
2546
|
-
printed_update = true;
|
|
2547
|
-
}
|
|
2548
|
-
} else {
|
|
2549
|
-
update_handle = Some(handle);
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
let path_str = format_path_with_tilde(&session.current_dir);
|
|
2554
|
-
let model_str = active_model(&session.config.ai_provider, &session.config).to_owned();
|
|
2555
|
-
|
|
2556
|
-
if let Some(input) = read_line_interactive(
|
|
2557
|
-
&session.config.ai_provider,
|
|
2558
|
-
&model_str,
|
|
2559
|
-
&path_str,
|
|
2560
|
-
&session.current_dir,
|
|
2561
|
-
)? {
|
|
2562
|
-
if let Some(uri) = input.pasted_image {
|
|
2563
|
-
if let Some(ref mut current) = session.pending_image {
|
|
2564
|
-
current.push(' ');
|
|
2565
|
-
current.push_str(&uri);
|
|
2566
|
-
} else {
|
|
2567
|
-
session.pending_image = Some(uri);
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
let query_str = input.text.trim().to_owned();
|
|
2571
|
-
if query_str.is_empty() {
|
|
2572
|
-
continue;
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
|
-
if query_str.starts_with('$') {
|
|
2576
|
-
let (skill_word, task_part) = query_str
|
|
2577
|
-
.split_once(char::is_whitespace)
|
|
2578
|
-
.map(|(s, t)| (s, t.trim()))
|
|
2579
|
-
.unwrap_or((&query_str, ""));
|
|
2580
|
-
|
|
2581
|
-
let skill_name = skill_word.trim_start_matches('$').to_lowercase();
|
|
2582
|
-
let skills = load_all_available_skills(&session.current_dir);
|
|
2583
|
-
let skill_opt = skills.iter().find(|s| s.name.to_lowercase() == skill_name);
|
|
2584
|
-
|
|
2585
|
-
if let Some(skill) = skill_opt {
|
|
2586
|
-
println!("\n{BLUE}Skill: {}{RESET}", skill.name);
|
|
2587
|
-
if let Some(ref desc) = skill.description {
|
|
2588
|
-
println!("{DIM}{}{RESET}", desc);
|
|
2589
|
-
}
|
|
2590
|
-
println!("{DIM}────────────────────────────────────────────{RESET}");
|
|
2591
|
-
println!("{}", skill.content);
|
|
2592
|
-
println!("{DIM}────────────────────────────────────────────{RESET}\n");
|
|
2593
|
-
|
|
2594
|
-
if confirm("ต้องการ activate skill นี้ไหม? [y/N] ")? {
|
|
2595
|
-
let final_task = if task_part.is_empty() {
|
|
2596
|
-
print!("พิมพ์ Task ที่ต้องการให้ทำงานด้วย Skill นี้: ");
|
|
2597
|
-
let _ = io::stdout().flush();
|
|
2598
|
-
let mut input = String::new();
|
|
2599
|
-
io::stdin().read_line(&mut input)?;
|
|
2600
|
-
let input = input.trim().to_owned();
|
|
2601
|
-
if input.is_empty() {
|
|
2602
|
-
println!("{WARN}Cancelled: Task cannot be empty.{RESET}\n");
|
|
2603
|
-
continue;
|
|
2604
|
-
}
|
|
2605
|
-
input
|
|
2606
|
-
} else {
|
|
2607
|
-
task_part.to_owned()
|
|
2608
|
-
};
|
|
2609
|
-
|
|
2610
|
-
let task_with_skill = format!(
|
|
2611
|
-
"=== ACTIVATED SKILL: {} ===\n\
|
|
2612
|
-
{}\n\
|
|
2613
|
-
===========================\n\n\
|
|
2614
|
-
Task: {}",
|
|
2615
|
-
skill.name, skill.content, final_task
|
|
2616
|
-
);
|
|
2617
|
-
|
|
2618
|
-
println!();
|
|
2619
|
-
if let Err(error) = run_code_agent_with_saved_image(
|
|
2620
|
-
&task_with_skill,
|
|
2621
|
-
&session.current_dir,
|
|
2622
|
-
&session.config,
|
|
2623
|
-
session.pending_image.take(),
|
|
2624
|
-
agent::AgentOptions {
|
|
2625
|
-
fast_mode: session.fast_mode,
|
|
2626
|
-
},
|
|
2627
|
-
)
|
|
2628
|
-
.await
|
|
2629
|
-
{
|
|
2630
|
-
println!("{ERROR}Error:{RESET} {error}\n");
|
|
2631
|
-
}
|
|
2632
|
-
} else {
|
|
2633
|
-
println!("{DIM}Cancelled.{RESET}\n");
|
|
2634
|
-
}
|
|
2635
|
-
} else {
|
|
2636
|
-
println!("{ERROR}Skill '{}' not found.{RESET}\n", skill_name);
|
|
2637
|
-
}
|
|
2638
|
-
continue;
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
// Run slash-command router
|
|
2642
|
-
match handle_slash_command(&mut session, &query_str).await {
|
|
2643
|
-
Some(SlashResult::Handled) => continue,
|
|
2644
|
-
Some(SlashResult::Exit) => {
|
|
2645
|
-
print_exit_message(&session);
|
|
2646
|
-
break;
|
|
2647
|
-
}
|
|
2648
|
-
Some(SlashResult::ForwardToAgent(task)) => {
|
|
2649
|
-
// Force code agent for /code forwarded tasks
|
|
2650
|
-
println!();
|
|
2651
|
-
if let Err(error) = run_code_agent_with_saved_image(
|
|
2652
|
-
&task,
|
|
2653
|
-
&session.current_dir,
|
|
2654
|
-
&session.config,
|
|
2655
|
-
session.pending_image.take(),
|
|
2656
|
-
agent::AgentOptions {
|
|
2657
|
-
fast_mode: session.fast_mode,
|
|
2658
|
-
},
|
|
2659
|
-
)
|
|
2660
|
-
.await
|
|
2661
|
-
{
|
|
2662
|
-
println!("{ERROR}Error:{RESET} {error}\n");
|
|
2663
|
-
}
|
|
2664
|
-
continue;
|
|
2665
|
-
}
|
|
2666
|
-
None => {} // Not a slash command, fall through
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
// Check if it's a /code or regular agent request
|
|
2670
|
-
if let Some(task) = query_str.strip_prefix("/code ") {
|
|
2671
|
-
println!();
|
|
2672
|
-
if let Err(error) = run_code_agent_with_saved_image(
|
|
2673
|
-
task.trim(),
|
|
2674
|
-
&session.current_dir,
|
|
2675
|
-
&session.config,
|
|
2676
|
-
session.pending_image.take(),
|
|
2677
|
-
agent::AgentOptions {
|
|
2678
|
-
fast_mode: session.fast_mode,
|
|
2679
|
-
},
|
|
2680
|
-
)
|
|
2681
|
-
.await
|
|
2682
|
-
{
|
|
2683
|
-
println!("{ERROR}Error:{RESET} {error}\n");
|
|
2684
|
-
}
|
|
2685
|
-
continue;
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// Regular agent loop (handles both chat and coding)
|
|
2689
|
-
if !query_str.starts_with("/chat ") {
|
|
2690
|
-
if let Err(error) = run_code_agent_with_saved_image(
|
|
2691
|
-
query_str.trim_start_matches("/chat "),
|
|
2692
|
-
&session.current_dir,
|
|
2693
|
-
&session.config,
|
|
2694
|
-
session.pending_image.take(),
|
|
2695
|
-
agent::AgentOptions {
|
|
2696
|
-
fast_mode: session.fast_mode,
|
|
2697
|
-
},
|
|
2698
|
-
)
|
|
2699
|
-
.await
|
|
2700
|
-
{
|
|
2701
|
-
println!("{ERROR}Error:{RESET} {error}\n");
|
|
2702
|
-
}
|
|
2703
|
-
continue;
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
// /chat explicit: use streaming chat with fallback
|
|
2707
|
-
let message = query_str
|
|
2708
|
-
.strip_prefix("/chat ")
|
|
2709
|
-
.unwrap_or(&query_str)
|
|
2710
|
-
.trim()
|
|
2711
|
-
.to_owned();
|
|
2712
|
-
|
|
2713
|
-
println!();
|
|
2714
|
-
print!(" {MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
|
|
2715
|
-
let _ = io::stdout().flush();
|
|
2716
|
-
|
|
2717
|
-
let mut system_instruction = "You are Mint, a cute and helpful AI assistant. You speak in a polite, friendly, and sweet Thai tone (using \"คุณ\", \"ค่ะ\", \"นะคะ\"). \
|
|
2718
|
-
You are running inside the Mint CLI interactive chat. \
|
|
2719
|
-
You have access to native system actions to help the user! If the user asks you to open a website, launch an app, read a file, list a folder, run code, run tests, or execute a local shell command, you can execute these actions by writing a special block at the very end of your response: \
|
|
2720
|
-
`[ACTION: <command> <arguments>]` \
|
|
2721
|
-
The available actions are: \
|
|
2722
|
-
- `[ACTION: open <url_or_path>]` to open a URL or a folder path. \
|
|
2723
|
-
- `[ACTION: open-app <app_name>]` to launch a desktop application. \
|
|
2724
|
-
- `[ACTION: read-file <file_path>]` to read the contents of a file. \
|
|
2725
|
-
- `[ACTION: read-folder <path>]` to list files/folders in a directory. \
|
|
2726
|
-
- `[ACTION: run-shell <command>]` to run a non-destructive local shell command after approval. \
|
|
2727
|
-
You may discuss mature personal topics at a non-explicit level, including adult relationships, sex education, intimacy, emotions, and feelings. Keep the tone respectful and supportive, avoid graphic sexual detail, and do not engage with sexual content involving minors, coercion, exploitation, or sexual violence. \
|
|
2728
|
-
Write the action block on a single line at the very end of your response.".to_string();
|
|
2729
|
-
if let Ok(memory) = MemoryStore::open_default()
|
|
2730
|
-
&& let Ok(Some(name)) = memory.get_profile("name")
|
|
2731
|
-
{
|
|
2732
|
-
system_instruction.push_str(&format!(
|
|
2733
|
-
"\nThe user's name is {}. Refer to them by their name when appropriate.",
|
|
2734
|
-
name
|
|
2735
|
-
));
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
let image_uri = session.pending_image.take();
|
|
2739
|
-
let sent_image = image_uri.clone();
|
|
2740
|
-
let mut first_chunk = true;
|
|
2741
|
-
let mut filter = ActionStreamFilter::new();
|
|
2742
|
-
let mut formatter = CliStreamFormatter::new();
|
|
2743
|
-
|
|
2744
|
-
let stream_result = orchestrate_chat_stream_with_fallback(
|
|
2745
|
-
&session.config,
|
|
2746
|
-
&ChatRequest {
|
|
2747
|
-
message: message.clone(),
|
|
2748
|
-
system_instruction,
|
|
2749
|
-
chat_id: Some(CHAT_CLI_ID.to_owned()),
|
|
2750
|
-
image_data_uri: image_uri,
|
|
2751
|
-
audio_data_uri: None,
|
|
2752
|
-
document_attachment: None,
|
|
2753
|
-
workspace_path: None,
|
|
2754
|
-
agent_id: None,
|
|
2755
|
-
},
|
|
2756
|
-
|chunk| {
|
|
2757
|
-
if first_chunk {
|
|
2758
|
-
first_chunk = false;
|
|
2759
|
-
print!("\r\x1b[2K {MINT}Mint:{RESET} ");
|
|
2760
|
-
}
|
|
2761
|
-
filter.process_chunk(&chunk, |text| {
|
|
2762
|
-
for c in text.chars() {
|
|
2763
|
-
formatter.process_char(c, |formatted_c| {
|
|
2764
|
-
print!("{}", formatted_c);
|
|
2765
|
-
});
|
|
2766
|
-
}
|
|
2767
|
-
});
|
|
2768
|
-
let _ = io::stdout().flush();
|
|
2769
|
-
},
|
|
2770
|
-
)
|
|
2771
|
-
.await;
|
|
2772
|
-
|
|
2773
|
-
let actions = filter.finalize(|text| {
|
|
2774
|
-
for c in text.chars() {
|
|
2775
|
-
formatter.process_char(c, |formatted_c| {
|
|
2776
|
-
print!("{}", formatted_c);
|
|
2777
|
-
});
|
|
2778
|
-
}
|
|
2779
|
-
});
|
|
2780
|
-
formatter.finalize(|formatted_c| {
|
|
2781
|
-
print!("{}", formatted_c);
|
|
2782
|
-
});
|
|
2783
|
-
let _ = io::stdout().flush();
|
|
2784
|
-
|
|
2785
|
-
match stream_result {
|
|
2786
|
-
Ok((response, fallback)) => {
|
|
2787
|
-
image::save_sent_image_after_send(sent_image.as_deref(), &message);
|
|
2788
|
-
if first_chunk {
|
|
2789
|
-
print!("\r\x1b[2K");
|
|
2790
|
-
let _ = io::stdout().flush();
|
|
2791
|
-
} else {
|
|
2792
|
-
println!("\n");
|
|
2793
|
-
let (tw, _) = crossterm::terminal::size().unwrap_or((80, 24));
|
|
2794
|
-
let width = tw as usize;
|
|
2795
|
-
// Show provider badge (with fallback indicator if applicable)
|
|
2796
|
-
let badge = if let Some(fb_provider) = &fallback {
|
|
2797
|
-
format!(
|
|
2798
|
-
"{DIM}{} • {} → fallback: {} • {}{RESET}",
|
|
2799
|
-
session.config.ai_provider,
|
|
2800
|
-
active_model(&session.config.ai_provider, &session.config),
|
|
2801
|
-
fb_provider,
|
|
2802
|
-
response.model
|
|
2803
|
-
)
|
|
2804
|
-
} else {
|
|
2805
|
-
format!("{DIM}{} • {}{RESET}", response.provider, response.model)
|
|
2806
|
-
};
|
|
2807
|
-
println!("{badge}");
|
|
2808
|
-
println!("{DIM}{}{RESET}\n", "─".repeat(width));
|
|
2809
|
-
}
|
|
2810
|
-
for action in actions {
|
|
2811
|
-
if let Err(e) = execute_action(&action, &session.config) {
|
|
2812
|
-
println!("{ERROR}Error executing action:{RESET} {e}\n");
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
Err(e) => {
|
|
2817
|
-
if first_chunk {
|
|
2818
|
-
print!("\r\x1b[2K");
|
|
2819
|
-
}
|
|
2820
|
-
println!("{ERROR}Error:{RESET} {e}\n");
|
|
2821
|
-
}
|
|
2822
|
-
}
|
|
2823
|
-
} else {
|
|
2824
|
-
print_exit_message(&session);
|
|
2825
|
-
break;
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
Ok(())
|
|
2830
|
-
}
|
|
2831
|
-
|
|
2832
|
-
fn print_exit_message(session: &InteractiveSession) {
|
|
2833
|
-
println!("\n{MINT}──────────────── Mint session closed ────────────────{RESET}");
|
|
2834
|
-
let clean_provider = format_provider_display_name(&session.config.ai_provider, &session.config);
|
|
2835
|
-
println!(
|
|
2836
|
-
"{DIM}Provider:{RESET} {} {DIM}• Model:{RESET} {}",
|
|
2837
|
-
clean_provider,
|
|
2838
|
-
active_model(&session.config.ai_provider, &session.config)
|
|
2839
|
-
);
|
|
2840
|
-
println!(
|
|
2841
|
-
"{DIM}Workspace:{RESET} {}",
|
|
2842
|
-
format_path_with_tilde(&session.current_dir)
|
|
2843
|
-
);
|
|
2844
|
-
println!("{DIM}Saved config stays available for the next Mint run.{RESET}");
|
|
2845
|
-
println!("{MINT}See you next time.{RESET}\n");
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
fn format_provider_display_name(provider: &str, config: &mint_core::MintConfig) -> String {
|
|
2849
|
-
if provider.starts_with("custom:") {
|
|
2850
|
-
let id = provider.strip_prefix("custom:").unwrap_or(provider);
|
|
2851
|
-
let cp = config.custom_providers.iter().find(|p| p.id == id);
|
|
2852
|
-
match cp {
|
|
2853
|
-
Some(p) if !p.display_name.is_empty() => format!("{} (Custom)", p.display_name),
|
|
2854
|
-
Some(p) => format!("{} (Custom)", p.id),
|
|
2855
|
-
None => format!("{} (Custom)", id),
|
|
2856
|
-
}
|
|
2857
|
-
} else {
|
|
2858
|
-
match provider {
|
|
2859
|
-
"gemini" => "Gemini".to_string(),
|
|
2860
|
-
"openai" => "OpenAI".to_string(),
|
|
2861
|
-
"openrouter" => "OpenRouter".to_string(),
|
|
2862
|
-
"deepseek" => "DeepSeek".to_string(),
|
|
2863
|
-
"anthropic" => "Claude".to_string(),
|
|
2864
|
-
"huggingface" => "HF".to_string(),
|
|
2865
|
-
"local_openai" => "Local".to_string(),
|
|
2866
|
-
"ollama" => "Ollama".to_string(),
|
|
2867
|
-
_ => {
|
|
2868
|
-
let mut chars = provider.chars();
|
|
2869
|
-
match chars.next() {
|
|
2870
|
-
None => String::new(),
|
|
2871
|
-
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
|
|
2879
|
-
("/cd", "Change active workspace directory"),
|
|
2880
|
-
("/clear", "Clear conversation history"),
|
|
2881
|
-
("/code", "Run in code-agent mode"),
|
|
2882
|
-
("/exit", "Exit Mint CLI"),
|
|
2883
|
-
("/fast", "Toggle fast mode (hide thinking traces)"),
|
|
2884
|
-
("/help", "Show help menu"),
|
|
2885
|
-
("/image", "Attach image from disk"),
|
|
2886
|
-
(
|
|
2887
|
-
"/image-provider",
|
|
2888
|
-
"List image gen providers or switch default provider",
|
|
2889
|
-
),
|
|
2890
|
-
("/learn", "Import persistent skill/instruction"),
|
|
2891
|
-
("/mcp", "List configured MCP servers"),
|
|
2892
|
-
("/memory", "Manage long-term memory store"),
|
|
2893
|
-
("/models", "List AI providers or switch active provider"),
|
|
2894
|
-
(
|
|
2895
|
-
"/multi-agent",
|
|
2896
|
-
"Show or toggle Multi-Agent Collaboration system",
|
|
2897
|
-
),
|
|
2898
|
-
("/paste", "Attach image from clipboard"),
|
|
2899
|
-
("/plugins", "List or generate plugins/skills"),
|
|
2900
|
-
("/skill add", "Add or install global skill file or folder"),
|
|
2901
|
-
("/stats", "Show session statistics"),
|
|
2902
|
-
];
|
|
2903
|
-
|
|
2904
|
-
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
|
2905
|
-
std::fs::create_dir_all(&dst)?;
|
|
2906
|
-
for entry in std::fs::read_dir(src)? {
|
|
2907
|
-
let entry = entry?;
|
|
2908
|
-
let ty = entry.file_type()?;
|
|
2909
|
-
if ty.is_dir() {
|
|
2910
|
-
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
|
2911
|
-
} else {
|
|
2912
|
-
std::fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
|
2913
|
-
}
|
|
2914
|
-
}
|
|
2915
|
-
Ok(())
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
fn load_all_available_skills(current_dir: &Path) -> Vec<mint_core::LearnedSkill> {
|
|
2919
|
-
let mut skills = Vec::new();
|
|
2920
|
-
if let Ok(memory) = MemoryStore::open_default() {
|
|
2921
|
-
if let Ok(s) = memory.learned_skills(100) {
|
|
2922
|
-
skills = s;
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
if let Some(home) = dirs::home_dir() {
|
|
2926
|
-
let global_skills_path = home.join(".config").join("mint").join("mint-skills");
|
|
2927
|
-
mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
|
|
2928
|
-
}
|
|
2929
|
-
let workspace_skills_path1 = current_dir.join(".agents").join("skills");
|
|
2930
|
-
mint_core::skills::load_skills_from_dir(&workspace_skills_path1, &mut skills);
|
|
2931
|
-
let workspace_skills_path2 = current_dir.join("skills");
|
|
2932
|
-
mint_core::skills::load_skills_from_dir(&workspace_skills_path2, &mut skills);
|
|
2933
|
-
|
|
2934
|
-
let mut unique_skills = std::collections::BTreeMap::new();
|
|
2935
|
-
for skill in skills {
|
|
2936
|
-
unique_skills.insert(skill.name.clone(), skill);
|
|
2937
|
-
}
|
|
2938
|
-
unique_skills.into_values().collect()
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
fn draw_input_box(
|
|
2942
|
-
input_chars: &[char],
|
|
2943
|
-
cursor_pos: usize,
|
|
2944
|
-
placeholder: &str,
|
|
2945
|
-
model: &str,
|
|
2946
|
-
path_str: &str,
|
|
2947
|
-
tab_base_input: Option<&str>,
|
|
2948
|
-
tab_index: Option<usize>,
|
|
2949
|
-
current_dir: &Path,
|
|
2950
|
-
) -> (usize, usize) {
|
|
2951
|
-
let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
|
|
2952
|
-
let width = term_width as usize;
|
|
2953
|
-
let prefix = "› ";
|
|
2954
|
-
let input_width = width.saturating_sub(2);
|
|
2955
|
-
let content_max_len = input_width.saturating_sub(prefix.chars().count());
|
|
2956
|
-
|
|
2957
|
-
let mut start_idx = 0;
|
|
2958
|
-
if input_chars.len() > content_max_len && cursor_pos >= content_max_len {
|
|
2959
|
-
start_idx = (cursor_pos + 5).saturating_sub(content_max_len);
|
|
2960
|
-
if start_idx + content_max_len > input_chars.len() {
|
|
2961
|
-
start_idx = input_chars.len() - content_max_len;
|
|
2962
|
-
}
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
|
-
let slice_chars = if input_chars.len() > content_max_len {
|
|
2966
|
-
&input_chars[start_idx..std::cmp::min(start_idx + content_max_len, input_chars.len())]
|
|
2967
|
-
} else {
|
|
2968
|
-
input_chars
|
|
2969
|
-
};
|
|
2970
|
-
let displayed_input: String = slice_chars.iter().collect();
|
|
2971
|
-
|
|
2972
|
-
let display_str = if displayed_input.is_empty() {
|
|
2973
|
-
format!("{DIM}{}\x1b[39m", placeholder)
|
|
2974
|
-
} else {
|
|
2975
|
-
format_placeholders(&displayed_input)
|
|
2976
|
-
};
|
|
2977
|
-
|
|
2978
|
-
let visible_len = if displayed_input.is_empty() {
|
|
2979
|
-
placeholder.chars().count()
|
|
2980
|
-
} else {
|
|
2981
|
-
string_visual_width(&displayed_input)
|
|
2982
|
-
};
|
|
2983
|
-
|
|
2984
|
-
let pad_len = content_max_len.saturating_sub(visible_len);
|
|
2985
|
-
let padding = " ".repeat(pad_len);
|
|
2986
|
-
let blank_line = " ".repeat(input_width);
|
|
2987
|
-
|
|
2988
|
-
println!(" {COMPOSER_BG}{blank_line}{RESET}");
|
|
2989
|
-
println!(
|
|
2990
|
-
" {COMPOSER_BG}{MINT}{}{RESET}{COMPOSER_BG}{}{}{RESET}",
|
|
2991
|
-
prefix, display_str, padding
|
|
2992
|
-
);
|
|
2993
|
-
println!(" {COMPOSER_BG}{blank_line}{RESET}");
|
|
2994
|
-
|
|
2995
|
-
let agent_str = format!(" {DIM}[Agent]{RESET} {MINT}{}{RESET}", model);
|
|
2996
|
-
let path_label = format!("path: {}", path_str);
|
|
2997
|
-
let agent_visible_len = " [Agent] ".len() + model.chars().count();
|
|
2998
|
-
let path_visible_len = path_label.chars().count();
|
|
2999
|
-
|
|
3000
|
-
let status_pad_len = (width - 1).saturating_sub(agent_visible_len + path_visible_len);
|
|
3001
|
-
let status_padding = " ".repeat(status_pad_len);
|
|
3002
|
-
|
|
3003
|
-
print!(
|
|
3004
|
-
"{}{}{}{}{}",
|
|
3005
|
-
agent_str, status_padding, DIM, path_label, RESET
|
|
3006
|
-
);
|
|
3007
|
-
|
|
3008
|
-
// Compute and draw suggestions
|
|
3009
|
-
let raw_input: String = input_chars.iter().collect();
|
|
3010
|
-
let search_query = tab_base_input.unwrap_or(&raw_input);
|
|
3011
|
-
let mut match_count = 0;
|
|
3012
|
-
if search_query.starts_with('/') {
|
|
3013
|
-
let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
|
|
3014
|
-
.iter()
|
|
3015
|
-
.filter(|(cmd, _)| cmd.starts_with(search_query))
|
|
3016
|
-
.collect();
|
|
3017
|
-
|
|
3018
|
-
if !matches.is_empty() {
|
|
3019
|
-
let total_pages = matches.len().div_ceil(5);
|
|
3020
|
-
let highlight_idx = tab_index.map(|idx| idx % matches.len());
|
|
3021
|
-
let selected_idx = highlight_idx.unwrap_or(0);
|
|
3022
|
-
let current_page = selected_idx / 5;
|
|
3023
|
-
let s_start_idx = current_page * 5;
|
|
3024
|
-
let s_end_idx = std::cmp::min(s_start_idx + 5, matches.len());
|
|
3025
|
-
match_count = s_end_idx - s_start_idx;
|
|
3026
|
-
|
|
3027
|
-
println!();
|
|
3028
|
-
println!(
|
|
3029
|
-
" {BLUE}Suggestions ({}/{}){RESET}",
|
|
3030
|
-
current_page + 1,
|
|
3031
|
-
total_pages
|
|
3032
|
-
);
|
|
3033
|
-
for i in s_start_idx..s_end_idx {
|
|
3034
|
-
let (cmd, desc) = matches[i];
|
|
3035
|
-
if Some(i) == highlight_idx {
|
|
3036
|
-
println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
|
|
3037
|
-
} else {
|
|
3038
|
-
println!(" {DIM}{:<16} - {}{RESET}", cmd, desc);
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
} else if search_query.starts_with('$') {
|
|
3043
|
-
// Parse skill name (excluding arguments)
|
|
3044
|
-
let skill_query = search_query
|
|
3045
|
-
.split_whitespace()
|
|
3046
|
-
.next()
|
|
3047
|
-
.unwrap_or(search_query);
|
|
3048
|
-
let prefix = &skill_query[1..].to_lowercase();
|
|
3049
|
-
let skills = load_all_available_skills(current_dir);
|
|
3050
|
-
let matches: Vec<_> = skills
|
|
3051
|
-
.iter()
|
|
3052
|
-
.filter(|skill| skill.name.to_lowercase().starts_with(prefix))
|
|
3053
|
-
.collect();
|
|
3054
|
-
|
|
3055
|
-
if !matches.is_empty() {
|
|
3056
|
-
let total_pages = matches.len().div_ceil(5);
|
|
3057
|
-
let highlight_idx = tab_index.map(|idx| idx % matches.len());
|
|
3058
|
-
let selected_idx = highlight_idx.unwrap_or(0);
|
|
3059
|
-
let current_page = selected_idx / 5;
|
|
3060
|
-
let s_start_idx = current_page * 5;
|
|
3061
|
-
let s_end_idx = std::cmp::min(s_start_idx + 5, matches.len());
|
|
3062
|
-
match_count = s_end_idx - s_start_idx;
|
|
3063
|
-
|
|
3064
|
-
println!();
|
|
3065
|
-
println!(
|
|
3066
|
-
" {BLUE}Suggestions ({}/{}){RESET}",
|
|
3067
|
-
current_page + 1,
|
|
3068
|
-
total_pages
|
|
3069
|
-
);
|
|
3070
|
-
for i in s_start_idx..s_end_idx {
|
|
3071
|
-
let skill = matches[i];
|
|
3072
|
-
let desc = skill
|
|
3073
|
-
.description
|
|
3074
|
-
.as_deref()
|
|
3075
|
-
.unwrap_or("No description provided");
|
|
3076
|
-
let max_desc_len = width.saturating_sub(35);
|
|
3077
|
-
let truncated_desc = if desc.chars().count() > max_desc_len {
|
|
3078
|
-
let mut s: String = desc.chars().take(max_desc_len.saturating_sub(3)).collect();
|
|
3079
|
-
s.push_str("...");
|
|
3080
|
-
s
|
|
3081
|
-
} else {
|
|
3082
|
-
desc.to_string()
|
|
3083
|
-
};
|
|
3084
|
-
|
|
3085
|
-
if Some(i) == highlight_idx {
|
|
3086
|
-
println!(
|
|
3087
|
-
" {BLUE}▶ ${:<20}{RESET} {MINT}[Skill]{RESET} {DIM}{}{RESET}",
|
|
3088
|
-
skill.name, truncated_desc
|
|
3089
|
-
);
|
|
3090
|
-
} else {
|
|
3091
|
-
println!(
|
|
3092
|
-
" {DIM}${:<20} [Skill] {}{RESET}",
|
|
3093
|
-
skill.name, truncated_desc
|
|
3094
|
-
);
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
|
-
let _ = io::stdout().flush();
|
|
3101
|
-
(match_count, start_idx)
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
fn input_cursor_column(input_chars: &[char], cursor_pos: usize) -> usize {
|
|
3105
|
-
let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
|
|
3106
|
-
let width = term_width as usize;
|
|
3107
|
-
let prefix = "› ";
|
|
3108
|
-
let input_width = width.saturating_sub(2);
|
|
3109
|
-
let content_max_len = input_width.saturating_sub(prefix.chars().count());
|
|
3110
|
-
|
|
3111
|
-
let mut start_idx = 0;
|
|
3112
|
-
if input_chars.len() > content_max_len && cursor_pos >= content_max_len {
|
|
3113
|
-
start_idx = (cursor_pos + 5).saturating_sub(content_max_len);
|
|
3114
|
-
if start_idx + content_max_len > input_chars.len() {
|
|
3115
|
-
start_idx = input_chars.len() - content_max_len;
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
let cursor_pos_clamped = std::cmp::min(cursor_pos, input_chars.len());
|
|
3120
|
-
let start_idx_clamped = std::cmp::min(start_idx, cursor_pos_clamped);
|
|
3121
|
-
let visual_cursor_pos: usize = input_chars[start_idx_clamped..cursor_pos_clamped]
|
|
3122
|
-
.iter()
|
|
3123
|
-
.copied()
|
|
3124
|
-
.map(char_visual_width)
|
|
3125
|
-
.sum();
|
|
3126
|
-
4 + visual_cursor_pos
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
fn position_input_cursor(input_chars: &[char], cursor_pos: usize, match_count: usize) {
|
|
3130
|
-
let up_lines = if match_count > 0 { 4 + match_count } else { 2 };
|
|
3131
|
-
print!(
|
|
3132
|
-
"\x1b[{}A\x1b[{}G",
|
|
3133
|
-
up_lines,
|
|
3134
|
-
input_cursor_column(input_chars, cursor_pos)
|
|
3135
|
-
);
|
|
3136
|
-
}
|
|
3137
|
-
|
|
3138
|
-
fn clear_input_box() {
|
|
3139
|
-
print!("\r\x1b[1A\x1b[J");
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
fn redraw_input_box(
|
|
3143
|
-
input_chars: &[char],
|
|
3144
|
-
cursor_pos: usize,
|
|
3145
|
-
placeholder: &str,
|
|
3146
|
-
model: &str,
|
|
3147
|
-
path_str: &str,
|
|
3148
|
-
tab_base_input: Option<&str>,
|
|
3149
|
-
tab_index: Option<usize>,
|
|
3150
|
-
current_dir: &Path,
|
|
3151
|
-
) {
|
|
3152
|
-
clear_input_box();
|
|
3153
|
-
let (match_count, _) = draw_input_box(
|
|
3154
|
-
input_chars,
|
|
3155
|
-
cursor_pos,
|
|
3156
|
-
placeholder,
|
|
3157
|
-
model,
|
|
3158
|
-
path_str,
|
|
3159
|
-
tab_base_input,
|
|
3160
|
-
tab_index,
|
|
3161
|
-
current_dir,
|
|
3162
|
-
);
|
|
3163
|
-
position_input_cursor(input_chars, cursor_pos, match_count);
|
|
3164
|
-
let _ = io::stdout().flush();
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
fn read_line_interactive(
|
|
3168
|
-
_provider: &str,
|
|
3169
|
-
model: &str,
|
|
3170
|
-
path_str: &str,
|
|
3171
|
-
current_dir: &Path,
|
|
3172
|
-
) -> io::Result<Option<InteractiveInput>> {
|
|
3173
|
-
use crossterm::event::{self, Event, KeyCode};
|
|
3174
|
-
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
|
3175
|
-
|
|
3176
|
-
let mut input_chars: Vec<char> = Vec::new();
|
|
3177
|
-
let mut cursor_pos = 0;
|
|
3178
|
-
let placeholder = "Ask anything...";
|
|
3179
|
-
let mut ctrl_d_pressed = false;
|
|
3180
|
-
let mut pasted_image: Option<String> = None;
|
|
3181
|
-
let mut paste_contents: Vec<(String, String)> = Vec::new();
|
|
3182
|
-
let mut last_paste_time: Option<std::time::Instant> = None;
|
|
3183
|
-
|
|
3184
|
-
// Track tab autocomplete state
|
|
3185
|
-
let mut tab_base_input: Option<String> = None;
|
|
3186
|
-
let mut tab_index: Option<usize> = None;
|
|
3187
|
-
|
|
3188
|
-
let (match_count, _) = draw_input_box(
|
|
3189
|
-
&input_chars,
|
|
3190
|
-
cursor_pos,
|
|
3191
|
-
placeholder,
|
|
3192
|
-
model,
|
|
3193
|
-
path_str,
|
|
3194
|
-
None,
|
|
3195
|
-
None,
|
|
3196
|
-
current_dir,
|
|
3197
|
-
);
|
|
3198
|
-
position_input_cursor(&input_chars, cursor_pos, match_count);
|
|
3199
|
-
let _ = io::stdout().flush();
|
|
3200
|
-
|
|
3201
|
-
enable_raw_mode()?;
|
|
3202
|
-
let _ = crossterm::execute!(io::stdout(), crossterm::event::EnableBracketedPaste);
|
|
3203
|
-
|
|
3204
|
-
let result = loop {
|
|
3205
|
-
if event::poll(std::time::Duration::from_millis(100))? {
|
|
3206
|
-
let ev = event::read()?;
|
|
3207
|
-
if let Event::Paste(text) = &ev {
|
|
3208
|
-
let clean_text = text.trim_end_matches(&['\r', '\n'][..]).to_string();
|
|
3209
|
-
let lines_count = clean_text.lines().count();
|
|
3210
|
-
if lines_count > 1 || clean_text.chars().count() > 100 {
|
|
3211
|
-
let paste_id = paste_contents.len() + 1;
|
|
3212
|
-
let placeholder_str = if lines_count > 1 {
|
|
3213
|
-
format!("[Pasted text #{} +{} lines]", paste_id, lines_count - 1)
|
|
3214
|
-
} else {
|
|
3215
|
-
format!("[Pasted text #{}]", paste_id)
|
|
3216
|
-
};
|
|
3217
|
-
paste_contents.push((placeholder_str.clone(), clean_text));
|
|
3218
|
-
for c in placeholder_str.chars() {
|
|
3219
|
-
input_chars.insert(cursor_pos, c);
|
|
3220
|
-
cursor_pos += 1;
|
|
3221
|
-
}
|
|
3222
|
-
} else {
|
|
3223
|
-
for c in clean_text.chars() {
|
|
3224
|
-
input_chars.insert(cursor_pos, c);
|
|
3225
|
-
cursor_pos += 1;
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
disable_raw_mode()?;
|
|
3229
|
-
redraw_input_box(
|
|
3230
|
-
&input_chars,
|
|
3231
|
-
cursor_pos,
|
|
3232
|
-
placeholder,
|
|
3233
|
-
model,
|
|
3234
|
-
path_str,
|
|
3235
|
-
None,
|
|
3236
|
-
None,
|
|
3237
|
-
current_dir,
|
|
3238
|
-
);
|
|
3239
|
-
enable_raw_mode()?;
|
|
3240
|
-
last_paste_time = Some(std::time::Instant::now());
|
|
3241
|
-
continue;
|
|
3242
|
-
}
|
|
3243
|
-
if let Event::Key(key_event) = ev
|
|
3244
|
-
&& key_event.kind == event::KeyEventKind::Press
|
|
3245
|
-
{
|
|
3246
|
-
let is_ctrl_d = matches!(key_event.code, KeyCode::Char('d'))
|
|
3247
|
-
&& key_event
|
|
3248
|
-
.modifiers
|
|
3249
|
-
.contains(crossterm::event::KeyModifiers::CONTROL);
|
|
3250
|
-
if ctrl_d_pressed && !is_ctrl_d {
|
|
3251
|
-
ctrl_d_pressed = false;
|
|
3252
|
-
disable_raw_mode()?;
|
|
3253
|
-
print!("\r\x1b[3B\r\x1b[2K\x1b[3A");
|
|
3254
|
-
print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
|
|
3255
|
-
let _ = io::stdout().flush();
|
|
3256
|
-
enable_raw_mode()?;
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
// Reset tab autocomplete state if any key other than Tab, Up, or Down is pressed
|
|
3260
|
-
if key_event.code != KeyCode::Tab
|
|
3261
|
-
&& key_event.code != KeyCode::Up
|
|
3262
|
-
&& key_event.code != KeyCode::Down
|
|
3263
|
-
{
|
|
3264
|
-
tab_base_input = None;
|
|
3265
|
-
tab_index = None;
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
match key_event.code {
|
|
3269
|
-
KeyCode::Char('d')
|
|
3270
|
-
if key_event
|
|
3271
|
-
.modifiers
|
|
3272
|
-
.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
|
3273
|
-
{
|
|
3274
|
-
if ctrl_d_pressed {
|
|
3275
|
-
disable_raw_mode()?;
|
|
3276
|
-
clear_input_box();
|
|
3277
|
-
let _ = io::stdout().flush();
|
|
3278
|
-
break None;
|
|
3279
|
-
} else {
|
|
3280
|
-
ctrl_d_pressed = true;
|
|
3281
|
-
disable_raw_mode()?;
|
|
3282
|
-
print!(
|
|
3283
|
-
"\r\x1b[3B\r\x1b[2K{WARN}Press Ctrl+D again to exit{RESET}\x1b[3A"
|
|
3284
|
-
);
|
|
3285
|
-
print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
|
|
3286
|
-
let _ = io::stdout().flush();
|
|
3287
|
-
enable_raw_mode()?;
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
KeyCode::Char('v')
|
|
3291
|
-
if key_event
|
|
3292
|
-
.modifiers
|
|
3293
|
-
.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
|
3294
|
-
{
|
|
3295
|
-
if let Ok(Some(uri)) = image::read_clipboard_image() {
|
|
3296
|
-
if let Some(ref mut current) = pasted_image {
|
|
3297
|
-
current.push(' ');
|
|
3298
|
-
current.push_str(&uri);
|
|
3299
|
-
} else {
|
|
3300
|
-
pasted_image = Some(uri);
|
|
3301
|
-
}
|
|
3302
|
-
insert_image_placeholder(&mut input_chars, &mut cursor_pos);
|
|
3303
|
-
|
|
3304
|
-
disable_raw_mode()?;
|
|
3305
|
-
redraw_input_box(
|
|
3306
|
-
&input_chars,
|
|
3307
|
-
cursor_pos,
|
|
3308
|
-
placeholder,
|
|
3309
|
-
model,
|
|
3310
|
-
path_str,
|
|
3311
|
-
None,
|
|
3312
|
-
None,
|
|
3313
|
-
current_dir,
|
|
3314
|
-
);
|
|
3315
|
-
enable_raw_mode()?;
|
|
3316
|
-
}
|
|
3317
|
-
}
|
|
3318
|
-
KeyCode::Char(c) if input_chars.len() < 10000 => {
|
|
3319
|
-
input_chars.insert(cursor_pos, c);
|
|
3320
|
-
cursor_pos += 1;
|
|
3321
|
-
|
|
3322
|
-
disable_raw_mode()?;
|
|
3323
|
-
redraw_input_box(
|
|
3324
|
-
&input_chars,
|
|
3325
|
-
cursor_pos,
|
|
3326
|
-
placeholder,
|
|
3327
|
-
model,
|
|
3328
|
-
path_str,
|
|
3329
|
-
None,
|
|
3330
|
-
None,
|
|
3331
|
-
current_dir,
|
|
3332
|
-
);
|
|
3333
|
-
enable_raw_mode()?;
|
|
3334
|
-
}
|
|
3335
|
-
KeyCode::Backspace if cursor_pos > 0 => {
|
|
3336
|
-
cursor_pos -= 1;
|
|
3337
|
-
input_chars.remove(cursor_pos);
|
|
3338
|
-
|
|
3339
|
-
disable_raw_mode()?;
|
|
3340
|
-
redraw_input_box(
|
|
3341
|
-
&input_chars,
|
|
3342
|
-
cursor_pos,
|
|
3343
|
-
placeholder,
|
|
3344
|
-
model,
|
|
3345
|
-
path_str,
|
|
3346
|
-
None,
|
|
3347
|
-
None,
|
|
3348
|
-
current_dir,
|
|
3349
|
-
);
|
|
3350
|
-
enable_raw_mode()?;
|
|
3351
|
-
}
|
|
3352
|
-
KeyCode::Tab => {
|
|
3353
|
-
let base = match &tab_base_input {
|
|
3354
|
-
Some(b) => b.clone(),
|
|
3355
|
-
None => {
|
|
3356
|
-
let current_str: String = input_chars.iter().collect();
|
|
3357
|
-
tab_base_input = Some(current_str.clone());
|
|
3358
|
-
current_str
|
|
3359
|
-
}
|
|
3360
|
-
};
|
|
3361
|
-
|
|
3362
|
-
if base.starts_with('/') {
|
|
3363
|
-
let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
|
|
3364
|
-
.iter()
|
|
3365
|
-
.filter(|(cmd, _)| cmd.starts_with(&base))
|
|
3366
|
-
.collect();
|
|
3367
|
-
|
|
3368
|
-
if !matches.is_empty() {
|
|
3369
|
-
let idx = tab_index.unwrap_or(0) % matches.len();
|
|
3370
|
-
let completed = format!("{} ", matches[idx].0);
|
|
3371
|
-
input_chars = completed.chars().collect();
|
|
3372
|
-
cursor_pos = input_chars.len();
|
|
3373
|
-
|
|
3374
|
-
// Highlight currently completed item in suggestions
|
|
3375
|
-
let current_highlight = Some(idx);
|
|
3376
|
-
tab_index = Some(idx + 1);
|
|
3377
|
-
|
|
3378
|
-
disable_raw_mode()?;
|
|
3379
|
-
redraw_input_box(
|
|
3380
|
-
&input_chars,
|
|
3381
|
-
cursor_pos,
|
|
3382
|
-
placeholder,
|
|
3383
|
-
model,
|
|
3384
|
-
path_str,
|
|
3385
|
-
Some(&base),
|
|
3386
|
-
current_highlight,
|
|
3387
|
-
current_dir,
|
|
3388
|
-
);
|
|
3389
|
-
enable_raw_mode()?;
|
|
3390
|
-
}
|
|
3391
|
-
} else if base.starts_with('$') {
|
|
3392
|
-
let skill_query = base.split_whitespace().next().unwrap_or(&base);
|
|
3393
|
-
let prefix = &skill_query[1..].to_lowercase();
|
|
3394
|
-
let skills = load_all_available_skills(current_dir);
|
|
3395
|
-
let matches: Vec<_> = skills
|
|
3396
|
-
.iter()
|
|
3397
|
-
.filter(|s| s.name.to_lowercase().starts_with(prefix))
|
|
3398
|
-
.collect();
|
|
3399
|
-
|
|
3400
|
-
if !matches.is_empty() {
|
|
3401
|
-
let idx = tab_index.unwrap_or(0) % matches.len();
|
|
3402
|
-
let completed = format!("${} ", matches[idx].name);
|
|
3403
|
-
input_chars = completed.chars().collect();
|
|
3404
|
-
cursor_pos = input_chars.len();
|
|
3405
|
-
|
|
3406
|
-
let current_highlight = Some(idx);
|
|
3407
|
-
tab_index = Some(idx + 1);
|
|
3408
|
-
|
|
3409
|
-
disable_raw_mode()?;
|
|
3410
|
-
redraw_input_box(
|
|
3411
|
-
&input_chars,
|
|
3412
|
-
cursor_pos,
|
|
3413
|
-
placeholder,
|
|
3414
|
-
model,
|
|
3415
|
-
path_str,
|
|
3416
|
-
Some(&base),
|
|
3417
|
-
current_highlight,
|
|
3418
|
-
current_dir,
|
|
3419
|
-
);
|
|
3420
|
-
enable_raw_mode()?;
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
KeyCode::Down => {
|
|
3425
|
-
let base = match &tab_base_input {
|
|
3426
|
-
Some(b) => b.clone(),
|
|
3427
|
-
None => {
|
|
3428
|
-
let current_str: String = input_chars.iter().collect();
|
|
3429
|
-
tab_base_input = Some(current_str.clone());
|
|
3430
|
-
current_str
|
|
3431
|
-
}
|
|
3432
|
-
};
|
|
3433
|
-
|
|
3434
|
-
if base.starts_with('/') {
|
|
3435
|
-
let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
|
|
3436
|
-
.iter()
|
|
3437
|
-
.filter(|(cmd, _)| cmd.starts_with(&base))
|
|
3438
|
-
.collect();
|
|
3439
|
-
|
|
3440
|
-
if !matches.is_empty() {
|
|
3441
|
-
let new_idx = match tab_index {
|
|
3442
|
-
Some(idx) => (idx + 1) % matches.len(),
|
|
3443
|
-
None => 0,
|
|
3444
|
-
};
|
|
3445
|
-
tab_index = Some(new_idx);
|
|
3446
|
-
let completed = format!("{} ", matches[new_idx].0);
|
|
3447
|
-
input_chars = completed.chars().collect();
|
|
3448
|
-
cursor_pos = input_chars.len();
|
|
3449
|
-
|
|
3450
|
-
disable_raw_mode()?;
|
|
3451
|
-
redraw_input_box(
|
|
3452
|
-
&input_chars,
|
|
3453
|
-
cursor_pos,
|
|
3454
|
-
placeholder,
|
|
3455
|
-
model,
|
|
3456
|
-
path_str,
|
|
3457
|
-
Some(&base),
|
|
3458
|
-
Some(new_idx),
|
|
3459
|
-
current_dir,
|
|
3460
|
-
);
|
|
3461
|
-
enable_raw_mode()?;
|
|
3462
|
-
}
|
|
3463
|
-
} else if base.starts_with('$') {
|
|
3464
|
-
let skill_query = base.split_whitespace().next().unwrap_or(&base);
|
|
3465
|
-
let prefix = &skill_query[1..].to_lowercase();
|
|
3466
|
-
let skills = load_all_available_skills(current_dir);
|
|
3467
|
-
let matches: Vec<_> = skills
|
|
3468
|
-
.iter()
|
|
3469
|
-
.filter(|s| s.name.to_lowercase().starts_with(prefix))
|
|
3470
|
-
.collect();
|
|
3471
|
-
|
|
3472
|
-
if !matches.is_empty() {
|
|
3473
|
-
let new_idx = match tab_index {
|
|
3474
|
-
Some(idx) => (idx + 1) % matches.len(),
|
|
3475
|
-
None => 0,
|
|
3476
|
-
};
|
|
3477
|
-
tab_index = Some(new_idx);
|
|
3478
|
-
let completed = format!("${} ", matches[new_idx].name);
|
|
3479
|
-
input_chars = completed.chars().collect();
|
|
3480
|
-
cursor_pos = input_chars.len();
|
|
3481
|
-
|
|
3482
|
-
disable_raw_mode()?;
|
|
3483
|
-
redraw_input_box(
|
|
3484
|
-
&input_chars,
|
|
3485
|
-
cursor_pos,
|
|
3486
|
-
placeholder,
|
|
3487
|
-
model,
|
|
3488
|
-
path_str,
|
|
3489
|
-
Some(&base),
|
|
3490
|
-
Some(new_idx),
|
|
3491
|
-
current_dir,
|
|
3492
|
-
);
|
|
3493
|
-
enable_raw_mode()?;
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
KeyCode::Up => {
|
|
3498
|
-
let base = match &tab_base_input {
|
|
3499
|
-
Some(b) => b.clone(),
|
|
3500
|
-
None => {
|
|
3501
|
-
let current_str: String = input_chars.iter().collect();
|
|
3502
|
-
tab_base_input = Some(current_str.clone());
|
|
3503
|
-
current_str
|
|
3504
|
-
}
|
|
3505
|
-
};
|
|
3506
|
-
|
|
3507
|
-
if base.starts_with('/') {
|
|
3508
|
-
let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
|
|
3509
|
-
.iter()
|
|
3510
|
-
.filter(|(cmd, _)| cmd.starts_with(&base))
|
|
3511
|
-
.collect();
|
|
3512
|
-
|
|
3513
|
-
if !matches.is_empty() {
|
|
3514
|
-
let new_idx = match tab_index {
|
|
3515
|
-
Some(idx) => {
|
|
3516
|
-
if idx == 0 {
|
|
3517
|
-
matches.len() - 1
|
|
3518
|
-
} else {
|
|
3519
|
-
idx - 1
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
None => matches.len() - 1,
|
|
3523
|
-
};
|
|
3524
|
-
tab_index = Some(new_idx);
|
|
3525
|
-
let completed = format!("{} ", matches[new_idx].0);
|
|
3526
|
-
input_chars = completed.chars().collect();
|
|
3527
|
-
cursor_pos = input_chars.len();
|
|
3528
|
-
|
|
3529
|
-
disable_raw_mode()?;
|
|
3530
|
-
redraw_input_box(
|
|
3531
|
-
&input_chars,
|
|
3532
|
-
cursor_pos,
|
|
3533
|
-
placeholder,
|
|
3534
|
-
model,
|
|
3535
|
-
path_str,
|
|
3536
|
-
Some(&base),
|
|
3537
|
-
Some(new_idx),
|
|
3538
|
-
current_dir,
|
|
3539
|
-
);
|
|
3540
|
-
enable_raw_mode()?;
|
|
3541
|
-
}
|
|
3542
|
-
} else if base.starts_with('$') {
|
|
3543
|
-
let skill_query = base.split_whitespace().next().unwrap_or(&base);
|
|
3544
|
-
let prefix = &skill_query[1..].to_lowercase();
|
|
3545
|
-
let skills = load_all_available_skills(current_dir);
|
|
3546
|
-
let matches: Vec<_> = skills
|
|
3547
|
-
.iter()
|
|
3548
|
-
.filter(|s| s.name.to_lowercase().starts_with(prefix))
|
|
3549
|
-
.collect();
|
|
3550
|
-
|
|
3551
|
-
if !matches.is_empty() {
|
|
3552
|
-
let new_idx = match tab_index {
|
|
3553
|
-
Some(idx) => {
|
|
3554
|
-
if idx == 0 {
|
|
3555
|
-
matches.len() - 1
|
|
3556
|
-
} else {
|
|
3557
|
-
idx - 1
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
None => matches.len() - 1,
|
|
3561
|
-
};
|
|
3562
|
-
tab_index = Some(new_idx);
|
|
3563
|
-
let completed = format!("${} ", matches[new_idx].name);
|
|
3564
|
-
input_chars = completed.chars().collect();
|
|
3565
|
-
cursor_pos = input_chars.len();
|
|
3566
|
-
|
|
3567
|
-
disable_raw_mode()?;
|
|
3568
|
-
redraw_input_box(
|
|
3569
|
-
&input_chars,
|
|
3570
|
-
cursor_pos,
|
|
3571
|
-
placeholder,
|
|
3572
|
-
model,
|
|
3573
|
-
path_str,
|
|
3574
|
-
Some(&base),
|
|
3575
|
-
Some(new_idx),
|
|
3576
|
-
current_dir,
|
|
3577
|
-
);
|
|
3578
|
-
enable_raw_mode()?;
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
KeyCode::Left => {
|
|
3583
|
-
while cursor_pos > 0 {
|
|
3584
|
-
cursor_pos -= 1;
|
|
3585
|
-
if cursor_pos == 0 || !is_thai_zero_width(input_chars[cursor_pos]) {
|
|
3586
|
-
break;
|
|
3587
|
-
}
|
|
3588
|
-
}
|
|
3589
|
-
disable_raw_mode()?;
|
|
3590
|
-
redraw_input_box(
|
|
3591
|
-
&input_chars,
|
|
3592
|
-
cursor_pos,
|
|
3593
|
-
placeholder,
|
|
3594
|
-
model,
|
|
3595
|
-
path_str,
|
|
3596
|
-
None,
|
|
3597
|
-
None,
|
|
3598
|
-
current_dir,
|
|
3599
|
-
);
|
|
3600
|
-
enable_raw_mode()?;
|
|
3601
|
-
}
|
|
3602
|
-
KeyCode::Right => {
|
|
3603
|
-
while cursor_pos < input_chars.len() {
|
|
3604
|
-
cursor_pos += 1;
|
|
3605
|
-
if cursor_pos == input_chars.len()
|
|
3606
|
-
|| !is_thai_zero_width(input_chars[cursor_pos])
|
|
3607
|
-
{
|
|
3608
|
-
break;
|
|
3609
|
-
}
|
|
3610
|
-
}
|
|
3611
|
-
disable_raw_mode()?;
|
|
3612
|
-
redraw_input_box(
|
|
3613
|
-
&input_chars,
|
|
3614
|
-
cursor_pos,
|
|
3615
|
-
placeholder,
|
|
3616
|
-
model,
|
|
3617
|
-
path_str,
|
|
3618
|
-
None,
|
|
3619
|
-
None,
|
|
3620
|
-
current_dir,
|
|
3621
|
-
);
|
|
3622
|
-
enable_raw_mode()?;
|
|
3623
|
-
}
|
|
3624
|
-
KeyCode::Enter => {
|
|
3625
|
-
if let Some(time) = last_paste_time
|
|
3626
|
-
&& time.elapsed() < std::time::Duration::from_millis(100)
|
|
3627
|
-
{
|
|
3628
|
-
continue;
|
|
3629
|
-
}
|
|
3630
|
-
disable_raw_mode()?;
|
|
3631
|
-
clear_input_box();
|
|
3632
|
-
let input_str: String = input_chars.iter().collect();
|
|
3633
|
-
|
|
3634
|
-
let mut expanded_str = input_str.clone();
|
|
3635
|
-
for (placeholder_str, content) in &paste_contents {
|
|
3636
|
-
expanded_str = expanded_str.replace(placeholder_str, content);
|
|
3637
|
-
}
|
|
3638
|
-
|
|
3639
|
-
println!(" {BLUE}You ›{RESET} {}", expanded_str);
|
|
3640
|
-
let _ = io::stdout().flush();
|
|
3641
|
-
|
|
3642
|
-
break Some(InteractiveInput {
|
|
3643
|
-
text: expanded_str,
|
|
3644
|
-
pasted_image,
|
|
3645
|
-
});
|
|
3646
|
-
}
|
|
3647
|
-
KeyCode::Esc => {
|
|
3648
|
-
disable_raw_mode()?;
|
|
3649
|
-
clear_input_box();
|
|
3650
|
-
let _ = io::stdout().flush();
|
|
3651
|
-
break None;
|
|
3652
|
-
}
|
|
3653
|
-
_ => {}
|
|
3654
|
-
}
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
};
|
|
3658
|
-
|
|
3659
|
-
let _ = crossterm::execute!(io::stdout(), crossterm::event::DisableBracketedPaste);
|
|
3660
|
-
Ok(result)
|
|
3661
|
-
}
|
|
3662
|
-
|
|
3663
|
-
fn insert_image_placeholder(input_chars: &mut Vec<char>, cursor_pos: &mut usize) {
|
|
3664
|
-
let input: String = input_chars.iter().collect();
|
|
3665
|
-
let mut idx = 1;
|
|
3666
|
-
while input.contains(&format!("[Image #{}]", idx)) {
|
|
3667
|
-
idx += 1;
|
|
3668
|
-
}
|
|
3669
|
-
let placeholder = format!("[Image #{}]", idx);
|
|
3670
|
-
let placeholder_chars = placeholder.chars().collect::<Vec<_>>();
|
|
3671
|
-
input_chars.splice(*cursor_pos..*cursor_pos, placeholder_chars.iter().copied());
|
|
3672
|
-
*cursor_pos += placeholder_chars.len();
|
|
3673
|
-
}
|
|
3674
|
-
|
|
3675
|
-
fn format_path_with_tilde(path: &std::path::Path) -> String {
|
|
3676
|
-
let path_str = path.to_string_lossy().to_string();
|
|
3677
|
-
if let Some(home) = dirs::home_dir() {
|
|
3678
|
-
let home_str = home.to_string_lossy().to_string();
|
|
3679
|
-
if path_str.starts_with(&home_str) {
|
|
3680
|
-
return path_str.replacen(&home_str, "~", 1);
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
path_str
|
|
3684
|
-
}
|
|
3685
|
-
|
|
3686
|
-
fn format_placeholders(s: &str) -> String {
|
|
3687
|
-
let mut result = String::new();
|
|
3688
|
-
let chars = s.chars().collect::<Vec<_>>();
|
|
3689
|
-
let mut i = 0;
|
|
3690
|
-
while i < chars.len() {
|
|
3691
|
-
if chars[i] == '[' && i + 12 < chars.len() {
|
|
3692
|
-
let slice: String = chars[i..i + 13].iter().collect();
|
|
3693
|
-
if slice == "[Pasted text #"
|
|
3694
|
-
&& let Some(end_offset) = chars[i..].iter().position(|&c| c == ']')
|
|
3695
|
-
{
|
|
3696
|
-
let end_idx = i + end_offset;
|
|
3697
|
-
let inside: String = chars[i + 1..end_idx].iter().collect();
|
|
3698
|
-
result.push('[');
|
|
3699
|
-
result.push_str(BLUE);
|
|
3700
|
-
result.push_str(&inside);
|
|
3701
|
-
result.push_str("\x1b[39m");
|
|
3702
|
-
result.push(']');
|
|
3703
|
-
i = end_idx + 1;
|
|
3704
|
-
continue;
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
result.push(chars[i]);
|
|
3708
|
-
i += 1;
|
|
3709
|
-
}
|
|
3710
|
-
result
|
|
3711
|
-
}
|
|
3712
|
-
|
|
3713
|
-
fn is_thai_zero_width(c: char) -> bool {
|
|
3714
|
-
let cp = c as u32;
|
|
3715
|
-
cp == 0x0E31 || (0x0E34..=0x0E3A).contains(&cp) || (0x0E47..=0x0E4E).contains(&cp)
|
|
3716
|
-
}
|
|
3717
|
-
|
|
3718
|
-
fn char_visual_width(c: char) -> usize {
|
|
3719
|
-
if is_thai_zero_width(c) { 0 } else { 1 }
|
|
3720
|
-
}
|
|
3721
|
-
|
|
3722
|
-
fn string_visual_width(s: &str) -> usize {
|
|
3723
|
-
s.chars().map(char_visual_width).sum()
|
|
3724
|
-
}
|
|
3725
|
-
|
|
3726
|
-
fn active_model<'a>(provider: &str, config: &'a mint_core::MintConfig) -> &'a str {
|
|
3727
|
-
match provider {
|
|
3728
|
-
"anthropic" => &config.anthropic_model,
|
|
3729
|
-
"openai" => &config.openai_model,
|
|
3730
|
-
"openrouter" => &config.openrouter_model,
|
|
3731
|
-
"deepseek" => &config.deepseek_model,
|
|
3732
|
-
"huggingface" => &config.hf_model,
|
|
3733
|
-
"local_openai" => &config.local_model_name,
|
|
3734
|
-
"ollama" => &config.ollama_model,
|
|
3735
|
-
p if p.starts_with("custom:") => {
|
|
3736
|
-
if let Some(id) = p.strip_prefix("custom:")
|
|
3737
|
-
&& let Some(selections) = config
|
|
3738
|
-
.extra
|
|
3739
|
-
.get("customModelSelections")
|
|
3740
|
-
.and_then(|v| v.as_object())
|
|
3741
|
-
&& let Some(m) = selections.get(id).and_then(|v| v.as_str())
|
|
3742
|
-
{
|
|
3743
|
-
return m;
|
|
3744
|
-
}
|
|
3745
|
-
config
|
|
3746
|
-
.resolve_custom_provider(p)
|
|
3747
|
-
.and_then(|cp| cp.models.first())
|
|
3748
|
-
.map(|m| m.model_id.as_str())
|
|
3749
|
-
.unwrap_or("")
|
|
3750
|
-
}
|
|
3751
|
-
_ => &config.gemini_model,
|
|
3752
|
-
}
|
|
3753
|
-
}
|
|
3754
|
-
|
|
3755
|
-
fn configured(config: &mint_core::MintConfig, keys: &[&str]) -> bool {
|
|
3756
|
-
keys.iter().all(|key| {
|
|
3757
|
-
config
|
|
3758
|
-
.extra
|
|
3759
|
-
.get(*key)
|
|
3760
|
-
.and_then(serde_json::Value::as_str)
|
|
3761
|
-
.is_some_and(|value| !value.trim().is_empty())
|
|
3762
|
-
})
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
fn edit_content(
|
|
3766
|
-
content: Option<String>,
|
|
3767
|
-
from_file: Option<PathBuf>,
|
|
3768
|
-
config: &MintConfig,
|
|
3769
|
-
) -> Result<String> {
|
|
3770
|
-
match from_file {
|
|
3771
|
-
Some(path) => {
|
|
3772
|
-
let path = assert_path_capability(&path, Capability::Read, config)?;
|
|
3773
|
-
Ok(fs::read_to_string(path)?)
|
|
3774
|
-
}
|
|
3775
|
-
None => Ok(content.unwrap_or_default()),
|
|
3776
|
-
}
|
|
3777
|
-
}
|
|
3778
|
-
|
|
3779
|
-
fn file_edits(values: &[String], config: &MintConfig) -> Result<Vec<CodeEdit>> {
|
|
3780
|
-
values
|
|
3781
|
-
.iter()
|
|
3782
|
-
.map(|value| {
|
|
3783
|
-
let (target, source) = value
|
|
3784
|
-
.split_once('=')
|
|
3785
|
-
.ok_or_else(|| anyhow::anyhow!("edit must use TARGET=SOURCE format"))?;
|
|
3786
|
-
Ok(CodeEdit {
|
|
3787
|
-
path: PathBuf::from(target),
|
|
3788
|
-
content: edit_content(None, Some(PathBuf::from(source)), config)?,
|
|
3789
|
-
})
|
|
3790
|
-
})
|
|
3791
|
-
.collect()
|
|
3792
|
-
}
|
|
3793
|
-
|
|
3794
|
-
fn open_system_handler(target: &str) -> Result<()> {
|
|
3795
|
-
#[cfg(target_os = "windows")]
|
|
3796
|
-
{
|
|
3797
|
-
std::process::Command::new("cmd")
|
|
3798
|
-
.args(&["/c", "start", "", target])
|
|
3799
|
-
.spawn()?;
|
|
3800
|
-
}
|
|
3801
|
-
#[cfg(target_os = "macos")]
|
|
3802
|
-
{
|
|
3803
|
-
std::process::Command::new("open").arg(target).spawn()?;
|
|
3804
|
-
}
|
|
3805
|
-
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
|
3806
|
-
{
|
|
3807
|
-
std::process::Command::new("xdg-open").arg(target).spawn()?;
|
|
3808
|
-
}
|
|
3809
|
-
Ok(())
|
|
3810
|
-
}
|
|
3811
|
-
|
|
3812
|
-
fn launch_desktop_app(name: &str) -> Result<()> {
|
|
3813
|
-
#[cfg(target_os = "windows")]
|
|
3814
|
-
{
|
|
3815
|
-
std::process::Command::new("cmd")
|
|
3816
|
-
.args(&["/c", "start", "", name])
|
|
3817
|
-
.spawn()?;
|
|
3818
|
-
}
|
|
3819
|
-
#[cfg(target_os = "macos")]
|
|
3820
|
-
{
|
|
3821
|
-
std::process::Command::new("open")
|
|
3822
|
-
.args(&["-a", name])
|
|
3823
|
-
.spawn()?;
|
|
3824
|
-
}
|
|
3825
|
-
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
|
3826
|
-
{
|
|
3827
|
-
if std::process::Command::new(name)
|
|
3828
|
-
.stdin(std::process::Stdio::null())
|
|
3829
|
-
.stdout(std::process::Stdio::null())
|
|
3830
|
-
.stderr(std::process::Stdio::null())
|
|
3831
|
-
.spawn()
|
|
3832
|
-
.is_ok()
|
|
3833
|
-
{
|
|
3834
|
-
return Ok(());
|
|
3835
|
-
}
|
|
3836
|
-
let lower = name.to_lowercase();
|
|
3837
|
-
if std::process::Command::new("gtk-launch")
|
|
3838
|
-
.arg(&lower)
|
|
3839
|
-
.stdin(std::process::Stdio::null())
|
|
3840
|
-
.stdout(std::process::Stdio::null())
|
|
3841
|
-
.stderr(std::process::Stdio::null())
|
|
3842
|
-
.spawn()
|
|
3843
|
-
.is_ok()
|
|
3844
|
-
{
|
|
3845
|
-
return Ok(());
|
|
3846
|
-
}
|
|
3847
|
-
std::process::Command::new("xdg-open").arg(name).spawn()?;
|
|
3848
|
-
}
|
|
3849
|
-
Ok(())
|
|
3850
|
-
}
|
|
3851
|
-
|
|
3852
|
-
fn read_file_content(path: &std::path::Path) -> Result<()> {
|
|
3853
|
-
let content = fs::read_to_string(path)?;
|
|
3854
|
-
println!("{}", content);
|
|
3855
|
-
Ok(())
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
|
-
fn read_folder_content(path: &std::path::Path) -> Result<()> {
|
|
3859
|
-
let entries = fs::read_dir(path)?;
|
|
3860
|
-
for entry in entries {
|
|
3861
|
-
let entry = entry?;
|
|
3862
|
-
let file_name = entry.file_name();
|
|
3863
|
-
let file_name_str = file_name.to_string_lossy();
|
|
3864
|
-
let metadata = entry.metadata()?;
|
|
3865
|
-
if metadata.is_dir() {
|
|
3866
|
-
println!("{MINT}{}/{}", file_name_str, RESET);
|
|
3867
|
-
} else {
|
|
3868
|
-
println!("{}", file_name_str);
|
|
3869
|
-
}
|
|
3870
|
-
}
|
|
3871
|
-
Ok(())
|
|
3872
|
-
}
|
|
3873
|
-
|
|
3874
|
-
pub static SESSION_APPROVED: std::sync::atomic::AtomicBool =
|
|
3875
|
-
std::sync::atomic::AtomicBool::new(false);
|
|
3876
|
-
|
|
3877
|
-
pub fn confirm(prompt: &str) -> Result<bool> {
|
|
3878
|
-
let clean_prompt = prompt
|
|
3879
|
-
.replace(" [y/N] ", "")
|
|
3880
|
-
.replace(" [y/N]", "")
|
|
3881
|
-
.trim()
|
|
3882
|
-
.to_string();
|
|
3883
|
-
|
|
3884
|
-
if SESSION_APPROVED.load(std::sync::atomic::Ordering::Relaxed) {
|
|
3885
|
-
println!(" {} {MINT}Approve (session-wide){RESET}", clean_prompt);
|
|
3886
|
-
return Ok(true);
|
|
3887
|
-
}
|
|
3888
|
-
|
|
3889
|
-
use crossterm::event::{self, Event, KeyCode};
|
|
3890
|
-
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
|
3891
|
-
use crossterm::tty::IsTty;
|
|
3892
|
-
|
|
3893
|
-
if !io::stdout().is_tty() || enable_raw_mode().is_err() {
|
|
3894
|
-
print!(" {} [y/N] ", clean_prompt);
|
|
3895
|
-
let _ = io::stdout().flush();
|
|
3896
|
-
let mut answer = String::new();
|
|
3897
|
-
io::stdin().read_line(&mut answer)?;
|
|
3898
|
-
return Ok(matches!(
|
|
3899
|
-
answer.trim().to_ascii_lowercase().as_str(),
|
|
3900
|
-
"y" | "yes"
|
|
3901
|
-
));
|
|
3902
|
-
}
|
|
3903
|
-
|
|
3904
|
-
let _ = disable_raw_mode();
|
|
3905
|
-
println!(" {}", clean_prompt);
|
|
3906
|
-
|
|
3907
|
-
let options = ["Approve", "Approve this session", "No"];
|
|
3908
|
-
let mut selected = 0;
|
|
3909
|
-
|
|
3910
|
-
let print_choices = |selected: usize| -> Result<()> {
|
|
3911
|
-
for (i, opt) in options.iter().enumerate() {
|
|
3912
|
-
if i == selected {
|
|
3913
|
-
println!(" {BLUE}❯ {}. {}{RESET}", i + 1, opt);
|
|
3914
|
-
} else {
|
|
3915
|
-
println!(" {DIM}{}. {}{RESET}", i + 1, opt);
|
|
3916
|
-
}
|
|
3917
|
-
}
|
|
3918
|
-
io::stdout().flush()?;
|
|
3919
|
-
Ok(())
|
|
3920
|
-
};
|
|
3921
|
-
|
|
3922
|
-
print_choices(selected)?;
|
|
3923
|
-
|
|
3924
|
-
let _ = enable_raw_mode();
|
|
3925
|
-
|
|
3926
|
-
let choice = loop {
|
|
3927
|
-
match event::poll(std::time::Duration::from_millis(100)) {
|
|
3928
|
-
Ok(true) => match event::read() {
|
|
3929
|
-
Ok(Event::Key(key_event)) => {
|
|
3930
|
-
if key_event.kind == event::KeyEventKind::Press {
|
|
3931
|
-
let is_ctrl_c = matches!(key_event.code, KeyCode::Char('c'))
|
|
3932
|
-
&& key_event
|
|
3933
|
-
.modifiers
|
|
3934
|
-
.contains(crossterm::event::KeyModifiers::CONTROL);
|
|
3935
|
-
if is_ctrl_c {
|
|
3936
|
-
break 2;
|
|
3937
|
-
}
|
|
3938
|
-
|
|
3939
|
-
match key_event.code {
|
|
3940
|
-
KeyCode::Up => {
|
|
3941
|
-
if selected > 0 {
|
|
3942
|
-
selected -= 1;
|
|
3943
|
-
} else {
|
|
3944
|
-
selected = options.len() - 1;
|
|
3945
|
-
}
|
|
3946
|
-
let _ = disable_raw_mode();
|
|
3947
|
-
print!("\x1b[{}A\x1b[J", options.len());
|
|
3948
|
-
let _ = print_choices(selected);
|
|
3949
|
-
let _ = enable_raw_mode();
|
|
3950
|
-
}
|
|
3951
|
-
KeyCode::Down => {
|
|
3952
|
-
if selected < options.len() - 1 {
|
|
3953
|
-
selected += 1;
|
|
3954
|
-
} else {
|
|
3955
|
-
selected = 0;
|
|
3956
|
-
}
|
|
3957
|
-
let _ = disable_raw_mode();
|
|
3958
|
-
print!("\x1b[{}A\x1b[J", options.len());
|
|
3959
|
-
let _ = print_choices(selected);
|
|
3960
|
-
let _ = enable_raw_mode();
|
|
3961
|
-
}
|
|
3962
|
-
KeyCode::Tab => {
|
|
3963
|
-
if selected < options.len() - 1 {
|
|
3964
|
-
selected += 1;
|
|
3965
|
-
} else {
|
|
3966
|
-
selected = 0;
|
|
3967
|
-
}
|
|
3968
|
-
let _ = disable_raw_mode();
|
|
3969
|
-
print!("\x1b[{}A\x1b[J", options.len());
|
|
3970
|
-
let _ = print_choices(selected);
|
|
3971
|
-
let _ = enable_raw_mode();
|
|
3972
|
-
}
|
|
3973
|
-
KeyCode::Char('1') | KeyCode::Char('a') | KeyCode::Char('y') => {
|
|
3974
|
-
break 0;
|
|
3975
|
-
}
|
|
3976
|
-
KeyCode::Char('2') | KeyCode::Char('s') => {
|
|
3977
|
-
break 1;
|
|
3978
|
-
}
|
|
3979
|
-
KeyCode::Char('3') | KeyCode::Char('n') | KeyCode::Char('c') => {
|
|
3980
|
-
break 2;
|
|
3981
|
-
}
|
|
3982
|
-
KeyCode::Enter => {
|
|
3983
|
-
break selected;
|
|
3984
|
-
}
|
|
3985
|
-
KeyCode::Esc => {
|
|
3986
|
-
break 2;
|
|
3987
|
-
}
|
|
3988
|
-
_ => {}
|
|
3989
|
-
}
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
Ok(_) => {}
|
|
3993
|
-
Err(_) => {
|
|
3994
|
-
break 2;
|
|
3995
|
-
}
|
|
3996
|
-
},
|
|
3997
|
-
Ok(false) => {}
|
|
3998
|
-
Err(_) => {
|
|
3999
|
-
break 2;
|
|
4000
|
-
}
|
|
4001
|
-
}
|
|
4002
|
-
};
|
|
4003
|
-
|
|
4004
|
-
let _ = disable_raw_mode();
|
|
4005
|
-
print!("\x1b[{}A\x1b[J", options.len() + 1);
|
|
4006
|
-
|
|
4007
|
-
let result_str = match choice {
|
|
4008
|
-
0 => format!("{MINT}Approve{RESET}"),
|
|
4009
|
-
1 => format!("{MINT}Approve this session{RESET}"),
|
|
4010
|
-
_ => format!("{ERROR}No{RESET}"),
|
|
4011
|
-
};
|
|
4012
|
-
println!(" {} {}", clean_prompt, result_str);
|
|
4013
|
-
let _ = io::stdout().flush();
|
|
4014
|
-
|
|
4015
|
-
match choice {
|
|
4016
|
-
0 => Ok(true),
|
|
4017
|
-
1 => {
|
|
4018
|
-
SESSION_APPROVED.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
4019
|
-
Ok(true)
|
|
4020
|
-
}
|
|
4021
|
-
_ => Ok(false),
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
|
|
4025
|
-
fn confirm_shell_execution() -> Result<bool> {
|
|
4026
|
-
confirm("Approve local shell execution? [y/N]")
|
|
4027
|
-
}
|
|
4028
|
-
|
|
4029
|
-
fn print_shell_output(output: &mint_core::ShellOutput) {
|
|
4030
|
-
if !output.stdout.is_empty() {
|
|
4031
|
-
print!("{}", output.stdout);
|
|
4032
|
-
}
|
|
4033
|
-
if !output.stderr.is_empty() {
|
|
4034
|
-
eprint!("{}", output.stderr);
|
|
4035
|
-
}
|
|
4036
|
-
if !output.stdout.ends_with('\n') && !output.stderr.ends_with('\n') {
|
|
4037
|
-
println!();
|
|
4038
|
-
}
|
|
4039
|
-
println!(
|
|
4040
|
-
"{DIM}[exit: {} | sandboxed: {}]{RESET}\n",
|
|
4041
|
-
output
|
|
4042
|
-
.status
|
|
4043
|
-
.map_or_else(|| "unknown".into(), |status| status.to_string()),
|
|
4044
|
-
output.sandboxed
|
|
1533
|
+
let spinner = ProgressBar::new_spinner();
|
|
1534
|
+
spinner.set_style(
|
|
1535
|
+
ProgressStyle::default_spinner()
|
|
1536
|
+
.tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
|
|
1537
|
+
.template("{spinner:.green} {msg}")
|
|
1538
|
+
.unwrap(),
|
|
4045
1539
|
);
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
|
|
4049
|
-
let Some((owner, repo_name)) = parse_github_url(repo) else {
|
|
4050
|
-
anyhow::bail!(
|
|
4051
|
-
"Invalid GitHub repository URL/format. Please use 'owner/repo' or a full GitHub URL."
|
|
4052
|
-
);
|
|
4053
|
-
};
|
|
4054
|
-
|
|
4055
|
-
println!(
|
|
1540
|
+
spinner.set_message(format!(
|
|
4056
1541
|
"Fetching information for {}/{} from GitHub...",
|
|
4057
1542
|
owner, repo_name
|
|
4058
|
-
);
|
|
1543
|
+
));
|
|
1544
|
+
spinner.enable_steady_tick(Duration::from_millis(80));
|
|
1545
|
+
|
|
4059
1546
|
let summary = match fetch_github_repo_summary(&owner, &repo_name).await {
|
|
4060
1547
|
Ok(s) => s,
|
|
4061
1548
|
Err(e) => {
|
|
1549
|
+
spinner.finish_and_clear();
|
|
4062
1550
|
anyhow::bail!(
|
|
4063
1551
|
"Failed to fetch repository summary: {}. Check that the repository is public and spelled correctly.",
|
|
4064
1552
|
e
|
|
@@ -4066,13 +1554,13 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
|
|
|
4066
1554
|
}
|
|
4067
1555
|
};
|
|
4068
1556
|
|
|
4069
|
-
|
|
1557
|
+
spinner.set_message("Analyzing repository with AI model...");
|
|
4070
1558
|
let prompt = format!(
|
|
4071
1559
|
"Here is the metadata, top-level directory structure, and README.md content for the GitHub repository {}/{}:\n\n{}\n\nBased on this information, please provide a high-level overview of what this repository is about, what tech stack it uses, its overall architecture, and how it is organized.",
|
|
4072
1560
|
owner, repo_name, summary
|
|
4073
1561
|
);
|
|
4074
1562
|
|
|
4075
|
-
let (response, _) = orchestrate_chat_with_fallback(
|
|
1563
|
+
let (response, _) = match orchestrate_chat_with_fallback(
|
|
4076
1564
|
config,
|
|
4077
1565
|
&ChatRequest {
|
|
4078
1566
|
message: prompt,
|
|
@@ -4080,12 +1568,22 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
|
|
|
4080
1568
|
chat_id: Some("github_review".to_string()),
|
|
4081
1569
|
image_data_uri: None,
|
|
4082
1570
|
audio_data_uri: None,
|
|
1571
|
+
video_data_uri: None,
|
|
4083
1572
|
document_attachment: None,
|
|
4084
1573
|
workspace_path: None,
|
|
4085
1574
|
agent_id: None,
|
|
4086
1575
|
},
|
|
4087
1576
|
)
|
|
4088
|
-
.await
|
|
1577
|
+
.await {
|
|
1578
|
+
Ok(res) => {
|
|
1579
|
+
spinner.finish_and_clear();
|
|
1580
|
+
res
|
|
1581
|
+
}
|
|
1582
|
+
Err(e) => {
|
|
1583
|
+
spinner.finish_and_clear();
|
|
1584
|
+
return Err(e.into());
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
4089
1587
|
|
|
4090
1588
|
println!(
|
|
4091
1589
|
"\n--- AI Repository Overview for {}/{} ---",
|
|
@@ -4095,19 +1593,3 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
|
|
|
4095
1593
|
println!("--------------------------------------------------");
|
|
4096
1594
|
Ok(())
|
|
4097
1595
|
}
|
|
4098
|
-
|
|
4099
|
-
#[cfg(test)]
|
|
4100
|
-
mod tests {
|
|
4101
|
-
use super::*;
|
|
4102
|
-
|
|
4103
|
-
#[test]
|
|
4104
|
-
fn inserts_one_image_placeholder_at_cursor() {
|
|
4105
|
-
let mut chars = "ask ".chars().collect::<Vec<_>>();
|
|
4106
|
-
let mut cursor = chars.len();
|
|
4107
|
-
|
|
4108
|
-
insert_image_placeholder(&mut chars, &mut cursor);
|
|
4109
|
-
|
|
4110
|
-
assert_eq!(chars.iter().collect::<String>(), "ask [Image #1]");
|
|
4111
|
-
assert_eq!(cursor, "ask [Image #1]".chars().count());
|
|
4112
|
-
}
|
|
4113
|
-
}
|