@pheem49/mint 1.8.2 → 1.9.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.toml +1 -1
- package/Release_Note.md +48 -3
- package/crates/mint-cli/src/agent.rs +95 -84
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/main.rs +806 -403
- package/crates/mint-cli/src/onboard.rs +108 -108
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-core/src/api_server.rs +57 -54
- package/crates/mint-core/src/browser.rs +1 -1
- package/crates/mint-core/src/chat.rs +123 -8
- package/crates/mint-core/src/code_tools.rs +18 -20
- package/crates/mint-core/src/config.rs +184 -17
- package/crates/mint-core/src/image_gen.rs +5 -5
- package/crates/mint-core/src/lib.rs +5 -5
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +104 -85
- package/crates/mint-core/src/pictures.rs +3 -4
- package/crates/mint-core/src/semantic.rs +1 -1
- package/crates/mint-core/src/shell.rs +1 -1
- package/crates/mint-core/src/skills.rs +46 -19
- package/package.json +1 -1
- package/shared-modules/linux-audio/fluidsynth2-static.json +9 -3
- package/shared-modules/linux-audio/fluidsynth2.json +8 -2
- package/shared-modules/linux-audio/lilv.json +26 -19
- package/shared-modules/linux-audio/lv2.json +0 -1
- package/src/renderer/shared/components/AgentActivityTable.tsx +25 -6
- package/src/renderer/shared/utils/fileIcons.ts +135 -0
- package/src/renderer/src/components/ChatPanel.tsx +25 -5
- package/src/renderer/src/components/MintDashboard.tsx +45 -2
- package/src/renderer/src/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
- package/src/renderer/src/components/SettingsWindow.tsx +68 -1
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/styles.css +16 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +25 -5
- package/src/renderer/src-web/components/MintDashboard.tsx +19 -1
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +21 -2
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
- package/src/renderer/src-web/components/SettingsWindow.tsx +75 -1
- package/src/renderer/src-web/css/styles.css +16 -0
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +41 -17
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -494,8 +494,8 @@ async fn main() -> Result<()> {
|
|
|
494
494
|
println!("{}", serde_json::to_string_pretty(&load_config()?)?)
|
|
495
495
|
}
|
|
496
496
|
ConfigCommand::Set { key, value } => {
|
|
497
|
-
let value =
|
|
498
|
-
.
|
|
497
|
+
let value =
|
|
498
|
+
serde_json::from_str(&value).unwrap_or(serde_json::Value::String(value));
|
|
499
499
|
println!(
|
|
500
500
|
"{}",
|
|
501
501
|
serde_json::to_string_pretty(&set_config_value(&key, value)?)?
|
|
@@ -579,7 +579,7 @@ async fn main() -> Result<()> {
|
|
|
579
579
|
|
|
580
580
|
// Start tailing the log file in a background task
|
|
581
581
|
let log_dir = dirs::config_dir()
|
|
582
|
-
.unwrap_or_else(
|
|
582
|
+
.unwrap_or_else(std::env::temp_dir)
|
|
583
583
|
.join("mint");
|
|
584
584
|
let log_file = log_dir.join("browser-automation.log");
|
|
585
585
|
|
|
@@ -591,40 +591,37 @@ async fn main() -> Result<()> {
|
|
|
591
591
|
use std::io::{BufRead, BufReader, Seek, SeekFrom};
|
|
592
592
|
let mut file_pos = 0;
|
|
593
593
|
loop {
|
|
594
|
-
if log_file_clone.exists()
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
if let Ok(pos) = log_file_clone.metadata().map(|m| m.len()) {
|
|
624
|
-
file_pos = pos;
|
|
625
|
-
}
|
|
594
|
+
if log_file_clone.exists()
|
|
595
|
+
&& let Ok(mut file) = std::fs::File::open(&log_file_clone)
|
|
596
|
+
&& file.seek(SeekFrom::Start(file_pos)).is_ok()
|
|
597
|
+
{
|
|
598
|
+
let reader = BufReader::new(file);
|
|
599
|
+
for line_str in reader.lines().flatten() {
|
|
600
|
+
if line_str.contains("[NAVIGATE]")
|
|
601
|
+
|| line_str.contains("[NAVIGATE_SUCCESS]")
|
|
602
|
+
{
|
|
603
|
+
println!("🌐 {line_str}");
|
|
604
|
+
} else if line_str.contains("[CLICK]")
|
|
605
|
+
|| line_str.contains("[CLICK_SUCCESS]")
|
|
606
|
+
{
|
|
607
|
+
println!("🖱️ {line_str}");
|
|
608
|
+
} else if line_str.contains("[TYPE]")
|
|
609
|
+
|| line_str.contains("[TYPE_SUCCESS]")
|
|
610
|
+
{
|
|
611
|
+
println!("⌨️ {line_str}");
|
|
612
|
+
} else if line_str.contains("[READ]")
|
|
613
|
+
|| line_str.contains("[READ_SUCCESS]")
|
|
614
|
+
{
|
|
615
|
+
println!("📖 {line_str}");
|
|
616
|
+
} else if line_str.contains("_ERROR]") {
|
|
617
|
+
println!("❌ {line_str}");
|
|
618
|
+
} else {
|
|
619
|
+
println!("📝 {line_str}");
|
|
626
620
|
}
|
|
627
621
|
}
|
|
622
|
+
if let Ok(pos) = log_file_clone.metadata().map(|m| m.len()) {
|
|
623
|
+
file_pos = pos;
|
|
624
|
+
}
|
|
628
625
|
}
|
|
629
626
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
630
627
|
}
|
|
@@ -705,20 +702,20 @@ async fn main() -> Result<()> {
|
|
|
705
702
|
home.join(".config").join("mint").join("mint-skills");
|
|
706
703
|
mint_core::skills::load_skills_from_dir(&global_skills_path, &mut skills);
|
|
707
704
|
}
|
|
708
|
-
if let Ok(root) = std::env::current_dir()
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
705
|
+
if let Ok(root) = std::env::current_dir()
|
|
706
|
+
&& let Ok(root) = root.canonicalize()
|
|
707
|
+
{
|
|
708
|
+
let workspace_skills_path1 = root.join(".agents").join("skills");
|
|
709
|
+
mint_core::skills::load_skills_from_dir(
|
|
710
|
+
&workspace_skills_path1,
|
|
711
|
+
&mut skills,
|
|
712
|
+
);
|
|
715
713
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
714
|
+
let workspace_skills_path2 = root.join("skills");
|
|
715
|
+
mint_core::skills::load_skills_from_dir(
|
|
716
|
+
&workspace_skills_path2,
|
|
717
|
+
&mut skills,
|
|
718
|
+
);
|
|
722
719
|
}
|
|
723
720
|
|
|
724
721
|
let mut unique_skills = std::collections::BTreeMap::new();
|
|
@@ -1153,10 +1150,10 @@ async fn main() -> Result<()> {
|
|
|
1153
1150
|
),
|
|
1154
1151
|
}
|
|
1155
1152
|
}
|
|
1156
|
-
if let Some(desc) = &result.description
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1153
|
+
if let Some(desc) = &result.description
|
|
1154
|
+
&& !desc.is_empty()
|
|
1155
|
+
{
|
|
1156
|
+
println!("\n{DIM}{desc}{RESET}");
|
|
1160
1157
|
}
|
|
1161
1158
|
}
|
|
1162
1159
|
Err(e) => eprintln!("{ERROR}Failed to save images: {e}{RESET}"),
|
|
@@ -1201,16 +1198,16 @@ async fn launch_mint_target(target: String) -> Result<()> {
|
|
|
1201
1198
|
path = p.parent();
|
|
1202
1199
|
}
|
|
1203
1200
|
}
|
|
1204
|
-
if found.is_none()
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
path = p.parent();
|
|
1201
|
+
if found.is_none()
|
|
1202
|
+
&& let Ok(cwd) = std::env::current_dir()
|
|
1203
|
+
{
|
|
1204
|
+
let mut path = Some(cwd.as_path());
|
|
1205
|
+
while let Some(p) = path {
|
|
1206
|
+
if p.join("package.json").exists() {
|
|
1207
|
+
found = Some(p.to_path_buf());
|
|
1208
|
+
break;
|
|
1213
1209
|
}
|
|
1210
|
+
path = p.parent();
|
|
1214
1211
|
}
|
|
1215
1212
|
}
|
|
1216
1213
|
found.ok_or_else(|| {
|
|
@@ -1219,7 +1216,7 @@ async fn launch_mint_target(target: String) -> Result<()> {
|
|
|
1219
1216
|
};
|
|
1220
1217
|
std::process::Command::new("npm")
|
|
1221
1218
|
.current_dir(&project_root)
|
|
1222
|
-
.args(
|
|
1219
|
+
.args(["run", "dev:web"])
|
|
1223
1220
|
.stdout(std::process::Stdio::null())
|
|
1224
1221
|
.stderr(std::process::Stdio::null())
|
|
1225
1222
|
.spawn()
|
|
@@ -1402,7 +1399,7 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1402
1399
|
rendered.push_str(&draw_border("┌", "┬", "┐", "─", &col_widths));
|
|
1403
1400
|
|
|
1404
1401
|
if let Some(ref header) = header_row {
|
|
1405
|
-
rendered.
|
|
1402
|
+
rendered.push('│');
|
|
1406
1403
|
for (i, col) in header.iter().enumerate() {
|
|
1407
1404
|
let width = unicode_width(col);
|
|
1408
1405
|
let padding = col_widths[i] - width;
|
|
@@ -1411,20 +1408,20 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1411
1408
|
col,
|
|
1412
1409
|
" ".repeat(padding + 1)
|
|
1413
1410
|
));
|
|
1414
|
-
rendered.
|
|
1411
|
+
rendered.push('│');
|
|
1415
1412
|
}
|
|
1416
1413
|
rendered.push('\n');
|
|
1417
1414
|
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1418
1415
|
}
|
|
1419
1416
|
|
|
1420
1417
|
for (r_idx, row) in data_rows.iter().enumerate() {
|
|
1421
|
-
rendered.
|
|
1418
|
+
rendered.push('│');
|
|
1422
1419
|
for i in 0..num_cols {
|
|
1423
1420
|
let col_val = row.get(i).cloned().unwrap_or_default();
|
|
1424
1421
|
let width = unicode_width(&col_val);
|
|
1425
1422
|
let padding = col_widths[i] - width;
|
|
1426
1423
|
rendered.push_str(&format!(" {}{}", col_val, " ".repeat(padding + 1)));
|
|
1427
|
-
rendered.
|
|
1424
|
+
rendered.push('│');
|
|
1428
1425
|
}
|
|
1429
1426
|
rendered.push('\n');
|
|
1430
1427
|
|
|
@@ -1433,7 +1430,7 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1433
1430
|
}
|
|
1434
1431
|
}
|
|
1435
1432
|
|
|
1436
|
-
rendered.push_str(
|
|
1433
|
+
rendered.push_str(draw_border("└", "┴", "┘", "─", &col_widths).trim_end());
|
|
1437
1434
|
rendered
|
|
1438
1435
|
}
|
|
1439
1436
|
|
|
@@ -1747,6 +1744,10 @@ async fn handle_slash_command(
|
|
|
1747
1744
|
("/paste [prompt]", "Attach image from clipboard"),
|
|
1748
1745
|
("Ctrl+V", "Paste clipboard image as [Image #1]"),
|
|
1749
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
|
+
),
|
|
1750
1751
|
(
|
|
1751
1752
|
"/plugins <name> [prompt]",
|
|
1752
1753
|
"Generate a skill.md file for a plugin/skill",
|
|
@@ -1810,8 +1811,12 @@ async fn handle_slash_command(
|
|
|
1810
1811
|
session.config.ai_provider = rest.to_owned();
|
|
1811
1812
|
match mint_core::save_config(&session.config) {
|
|
1812
1813
|
Ok(()) => {
|
|
1813
|
-
let active_model =
|
|
1814
|
-
|
|
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
|
+
);
|
|
1815
1820
|
|
|
1816
1821
|
// Save system event interaction in memory
|
|
1817
1822
|
if let Ok(memory) = MemoryStore::open_default() {
|
|
@@ -1842,7 +1847,11 @@ async fn handle_slash_command(
|
|
|
1842
1847
|
match mint_core::save_config(&session.config) {
|
|
1843
1848
|
Ok(()) => println!(
|
|
1844
1849
|
"{DIM}Multi-Agent collaboration set to: {}{RESET}\n",
|
|
1845
|
-
if session.config.enable_agent_collaboration {
|
|
1850
|
+
if session.config.enable_agent_collaboration {
|
|
1851
|
+
"Enabled"
|
|
1852
|
+
} else {
|
|
1853
|
+
"Disabled"
|
|
1854
|
+
}
|
|
1846
1855
|
),
|
|
1847
1856
|
Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
|
|
1848
1857
|
}
|
|
@@ -2093,6 +2102,73 @@ async fn handle_slash_command(
|
|
|
2093
2102
|
Some(SlashResult::Handled)
|
|
2094
2103
|
}
|
|
2095
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
|
+
|
|
2096
2172
|
"/memory" => {
|
|
2097
2173
|
let memory = match MemoryStore::open_default() {
|
|
2098
2174
|
Ok(m) => m,
|
|
@@ -2291,44 +2367,40 @@ async fn handle_slash_command(
|
|
|
2291
2367
|
|
|
2292
2368
|
// Scan local workspace skills directory
|
|
2293
2369
|
let local_skills_dir = session.current_dir.join("skills");
|
|
2294
|
-
if local_skills_dir.is_dir()
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
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));
|
|
2313
2385
|
}
|
|
2314
2386
|
}
|
|
2387
|
+
}
|
|
2315
2388
|
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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();
|
|
2321
2394
|
|
|
2322
|
-
|
|
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 {
|
|
2323
2400
|
println!(
|
|
2324
|
-
"
|
|
2401
|
+
" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
|
|
2402
|
+
file, file, file
|
|
2325
2403
|
);
|
|
2326
|
-
for file in inactive_files {
|
|
2327
|
-
println!(
|
|
2328
|
-
" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
|
|
2329
|
-
file, file, file
|
|
2330
|
-
);
|
|
2331
|
-
}
|
|
2332
2404
|
}
|
|
2333
2405
|
}
|
|
2334
2406
|
}
|
|
@@ -2387,7 +2459,8 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2387
2459
|
now.format("%M")
|
|
2388
2460
|
);
|
|
2389
2461
|
let version = env!("CARGO_PKG_VERSION");
|
|
2390
|
-
let
|
|
2462
|
+
let clean_provider_name = format_provider_display_name(provider, &config);
|
|
2463
|
+
let line1_text = format!("[Mint] v{} | Active AI: {}", version, clean_provider_name);
|
|
2391
2464
|
let line2_text = format!("{} • {}", date_time, model);
|
|
2392
2465
|
|
|
2393
2466
|
let len1 = line1_text.chars().count();
|
|
@@ -2409,7 +2482,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2409
2482
|
println!(
|
|
2410
2483
|
"{MINT}| \\/ (_)_ __ | |_ / __| | |_ _|{RESET} {DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
|
|
2411
2484
|
version,
|
|
2412
|
-
|
|
2485
|
+
clean_provider_name,
|
|
2413
2486
|
" ".repeat(content_width - len1)
|
|
2414
2487
|
);
|
|
2415
2488
|
println!(
|
|
@@ -2426,7 +2499,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2426
2499
|
println!(
|
|
2427
2500
|
"{DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
|
|
2428
2501
|
version,
|
|
2429
|
-
|
|
2502
|
+
clean_provider_name,
|
|
2430
2503
|
" ".repeat(content_width - len1)
|
|
2431
2504
|
);
|
|
2432
2505
|
println!(
|
|
@@ -2466,11 +2539,11 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2466
2539
|
loop {
|
|
2467
2540
|
if let Some(handle) = update_handle.take() {
|
|
2468
2541
|
if handle.is_finished() {
|
|
2469
|
-
if let Ok(Some((current, latest))) = handle.await
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2542
|
+
if let Ok(Some((current, latest))) = handle.await
|
|
2543
|
+
&& !printed_update
|
|
2544
|
+
{
|
|
2545
|
+
updater::print_update_notice(¤t, &latest);
|
|
2546
|
+
printed_update = true;
|
|
2474
2547
|
}
|
|
2475
2548
|
} else {
|
|
2476
2549
|
update_handle = Some(handle);
|
|
@@ -2480,9 +2553,12 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2480
2553
|
let path_str = format_path_with_tilde(&session.current_dir);
|
|
2481
2554
|
let model_str = active_model(&session.config.ai_provider, &session.config).to_owned();
|
|
2482
2555
|
|
|
2483
|
-
if let Some(input) =
|
|
2484
|
-
|
|
2485
|
-
|
|
2556
|
+
if let Some(input) = read_line_interactive(
|
|
2557
|
+
&session.config.ai_provider,
|
|
2558
|
+
&model_str,
|
|
2559
|
+
&path_str,
|
|
2560
|
+
&session.current_dir,
|
|
2561
|
+
)? {
|
|
2486
2562
|
if let Some(uri) = input.pasted_image {
|
|
2487
2563
|
if let Some(ref mut current) = session.pending_image {
|
|
2488
2564
|
current.push(' ');
|
|
@@ -2496,6 +2572,72 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2496
2572
|
continue;
|
|
2497
2573
|
}
|
|
2498
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
|
+
|
|
2499
2641
|
// Run slash-command router
|
|
2500
2642
|
match handle_slash_command(&mut session, &query_str).await {
|
|
2501
2643
|
Some(SlashResult::Handled) => continue,
|
|
@@ -2572,8 +2714,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2572
2714
|
print!(" {MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
|
|
2573
2715
|
let _ = io::stdout().flush();
|
|
2574
2716
|
|
|
2575
|
-
let mut system_instruction =
|
|
2576
|
-
"You are Mint, a cute and helpful AI assistant. You speak in a polite, friendly, and sweet Thai tone (using \"คุณ\", \"ค่ะ\", \"นะคะ\"). \
|
|
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 \"คุณ\", \"ค่ะ\", \"นะคะ\"). \
|
|
2577
2718
|
You are running inside the Mint CLI interactive chat. \
|
|
2578
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: \
|
|
2579
2720
|
`[ACTION: <command> <arguments>]` \
|
|
@@ -2584,15 +2725,14 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2584
2725
|
- `[ACTION: read-folder <path>]` to list files/folders in a directory. \
|
|
2585
2726
|
- `[ACTION: run-shell <command>]` to run a non-destructive local shell command after approval. \
|
|
2586
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. \
|
|
2587
|
-
Write the action block on a single line at the very end of your response."
|
|
2588
|
-
)
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
}
|
|
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
|
+
));
|
|
2596
2736
|
}
|
|
2597
2737
|
|
|
2598
2738
|
let image_uri = session.pending_image.take();
|
|
@@ -2691,9 +2831,10 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2691
2831
|
|
|
2692
2832
|
fn print_exit_message(session: &InteractiveSession) {
|
|
2693
2833
|
println!("\n{MINT}──────────────── Mint session closed ────────────────{RESET}");
|
|
2834
|
+
let clean_provider = format_provider_display_name(&session.config.ai_provider, &session.config);
|
|
2694
2835
|
println!(
|
|
2695
2836
|
"{DIM}Provider:{RESET} {} {DIM}• Model:{RESET} {}",
|
|
2696
|
-
|
|
2837
|
+
clean_provider,
|
|
2697
2838
|
active_model(&session.config.ai_provider, &session.config)
|
|
2698
2839
|
);
|
|
2699
2840
|
println!(
|
|
@@ -2704,6 +2845,36 @@ fn print_exit_message(session: &InteractiveSession) {
|
|
|
2704
2845
|
println!("{MINT}See you next time.{RESET}\n");
|
|
2705
2846
|
}
|
|
2706
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
|
+
|
|
2707
2878
|
const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
|
|
2708
2879
|
("/cd", "Change active workspace directory"),
|
|
2709
2880
|
("/clear", "Clear conversation history"),
|
|
@@ -2720,12 +2891,53 @@ const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
|
|
|
2720
2891
|
("/mcp", "List configured MCP servers"),
|
|
2721
2892
|
("/memory", "Manage long-term memory store"),
|
|
2722
2893
|
("/models", "List AI providers or switch active provider"),
|
|
2723
|
-
(
|
|
2894
|
+
(
|
|
2895
|
+
"/multi-agent",
|
|
2896
|
+
"Show or toggle Multi-Agent Collaboration system",
|
|
2897
|
+
),
|
|
2724
2898
|
("/paste", "Attach image from clipboard"),
|
|
2725
2899
|
("/plugins", "List or generate plugins/skills"),
|
|
2900
|
+
("/skill add", "Add or install global skill file or folder"),
|
|
2726
2901
|
("/stats", "Show session statistics"),
|
|
2727
2902
|
];
|
|
2728
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
|
+
|
|
2729
2941
|
fn draw_input_box(
|
|
2730
2942
|
input_chars: &[char],
|
|
2731
2943
|
cursor_pos: usize,
|
|
@@ -2734,6 +2946,7 @@ fn draw_input_box(
|
|
|
2734
2946
|
path_str: &str,
|
|
2735
2947
|
tab_base_input: Option<&str>,
|
|
2736
2948
|
tab_index: Option<usize>,
|
|
2949
|
+
current_dir: &Path,
|
|
2737
2950
|
) -> (usize, usize) {
|
|
2738
2951
|
let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
|
|
2739
2952
|
let width = term_width as usize;
|
|
@@ -2742,12 +2955,10 @@ fn draw_input_box(
|
|
|
2742
2955
|
let content_max_len = input_width.saturating_sub(prefix.chars().count());
|
|
2743
2956
|
|
|
2744
2957
|
let mut start_idx = 0;
|
|
2745
|
-
if input_chars.len() > content_max_len {
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
start_idx = input_chars.len() - content_max_len;
|
|
2750
|
-
}
|
|
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;
|
|
2751
2962
|
}
|
|
2752
2963
|
}
|
|
2753
2964
|
|
|
@@ -2805,17 +3016,21 @@ fn draw_input_box(
|
|
|
2805
3016
|
.collect();
|
|
2806
3017
|
|
|
2807
3018
|
if !matches.is_empty() {
|
|
2808
|
-
let total_pages =
|
|
3019
|
+
let total_pages = matches.len().div_ceil(5);
|
|
2809
3020
|
let highlight_idx = tab_index.map(|idx| idx % matches.len());
|
|
2810
3021
|
let selected_idx = highlight_idx.unwrap_or(0);
|
|
2811
3022
|
let current_page = selected_idx / 5;
|
|
2812
|
-
let
|
|
2813
|
-
let
|
|
2814
|
-
match_count =
|
|
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;
|
|
2815
3026
|
|
|
2816
3027
|
println!();
|
|
2817
|
-
println!(
|
|
2818
|
-
|
|
3028
|
+
println!(
|
|
3029
|
+
" {BLUE}Suggestions ({}/{}){RESET}",
|
|
3030
|
+
current_page + 1,
|
|
3031
|
+
total_pages
|
|
3032
|
+
);
|
|
3033
|
+
for i in s_start_idx..s_end_idx {
|
|
2819
3034
|
let (cmd, desc) = matches[i];
|
|
2820
3035
|
if Some(i) == highlight_idx {
|
|
2821
3036
|
println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
|
|
@@ -2824,6 +3039,62 @@ fn draw_input_box(
|
|
|
2824
3039
|
}
|
|
2825
3040
|
}
|
|
2826
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
|
+
}
|
|
2827
3098
|
}
|
|
2828
3099
|
|
|
2829
3100
|
let _ = io::stdout().flush();
|
|
@@ -2838,12 +3109,10 @@ fn input_cursor_column(input_chars: &[char], cursor_pos: usize) -> usize {
|
|
|
2838
3109
|
let content_max_len = input_width.saturating_sub(prefix.chars().count());
|
|
2839
3110
|
|
|
2840
3111
|
let mut start_idx = 0;
|
|
2841
|
-
if input_chars.len() > content_max_len {
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
start_idx = input_chars.len() - content_max_len;
|
|
2846
|
-
}
|
|
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;
|
|
2847
3116
|
}
|
|
2848
3117
|
}
|
|
2849
3118
|
|
|
@@ -2878,6 +3147,7 @@ fn redraw_input_box(
|
|
|
2878
3147
|
path_str: &str,
|
|
2879
3148
|
tab_base_input: Option<&str>,
|
|
2880
3149
|
tab_index: Option<usize>,
|
|
3150
|
+
current_dir: &Path,
|
|
2881
3151
|
) {
|
|
2882
3152
|
clear_input_box();
|
|
2883
3153
|
let (match_count, _) = draw_input_box(
|
|
@@ -2888,6 +3158,7 @@ fn redraw_input_box(
|
|
|
2888
3158
|
path_str,
|
|
2889
3159
|
tab_base_input,
|
|
2890
3160
|
tab_index,
|
|
3161
|
+
current_dir,
|
|
2891
3162
|
);
|
|
2892
3163
|
position_input_cursor(input_chars, cursor_pos, match_count);
|
|
2893
3164
|
let _ = io::stdout().flush();
|
|
@@ -2897,6 +3168,7 @@ fn read_line_interactive(
|
|
|
2897
3168
|
_provider: &str,
|
|
2898
3169
|
model: &str,
|
|
2899
3170
|
path_str: &str,
|
|
3171
|
+
current_dir: &Path,
|
|
2900
3172
|
) -> io::Result<Option<InteractiveInput>> {
|
|
2901
3173
|
use crossterm::event::{self, Event, KeyCode};
|
|
2902
3174
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
|
@@ -2913,7 +3185,16 @@ fn read_line_interactive(
|
|
|
2913
3185
|
let mut tab_base_input: Option<String> = None;
|
|
2914
3186
|
let mut tab_index: Option<usize> = None;
|
|
2915
3187
|
|
|
2916
|
-
let (match_count, _) = draw_input_box(
|
|
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
|
+
);
|
|
2917
3198
|
position_input_cursor(&input_chars, cursor_pos, match_count);
|
|
2918
3199
|
let _ = io::stdout().flush();
|
|
2919
3200
|
|
|
@@ -2953,70 +3234,177 @@ fn read_line_interactive(
|
|
|
2953
3234
|
path_str,
|
|
2954
3235
|
None,
|
|
2955
3236
|
None,
|
|
3237
|
+
current_dir,
|
|
2956
3238
|
);
|
|
2957
3239
|
enable_raw_mode()?;
|
|
2958
3240
|
last_paste_time = Some(std::time::Instant::now());
|
|
2959
3241
|
continue;
|
|
2960
3242
|
}
|
|
2961
|
-
if let Event::Key(key_event) = ev
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
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
|
|
2965
3271
|
.modifiers
|
|
2966
|
-
.contains(crossterm::event::KeyModifiers::CONTROL)
|
|
2967
|
-
|
|
2968
|
-
ctrl_d_pressed
|
|
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
|
+
|
|
2969
3322
|
disable_raw_mode()?;
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
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
|
+
);
|
|
2973
3333
|
enable_raw_mode()?;
|
|
2974
3334
|
}
|
|
3335
|
+
KeyCode::Backspace if cursor_pos > 0 => {
|
|
3336
|
+
cursor_pos -= 1;
|
|
3337
|
+
input_chars.remove(cursor_pos);
|
|
2975
3338
|
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
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()?;
|
|
2983
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);
|
|
2984
3377
|
|
|
2985
|
-
match key_event.code {
|
|
2986
|
-
KeyCode::Char('d')
|
|
2987
|
-
if key_event
|
|
2988
|
-
.modifiers
|
|
2989
|
-
.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
|
2990
|
-
{
|
|
2991
|
-
if ctrl_d_pressed {
|
|
2992
|
-
disable_raw_mode()?;
|
|
2993
|
-
clear_input_box();
|
|
2994
|
-
let _ = io::stdout().flush();
|
|
2995
|
-
break None;
|
|
2996
|
-
} else {
|
|
2997
|
-
ctrl_d_pressed = true;
|
|
2998
3378
|
disable_raw_mode()?;
|
|
2999
|
-
|
|
3000
|
-
|
|
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,
|
|
3001
3388
|
);
|
|
3002
|
-
print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
|
|
3003
|
-
let _ = io::stdout().flush();
|
|
3004
3389
|
enable_raw_mode()?;
|
|
3005
3390
|
}
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
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);
|
|
3020
3408
|
|
|
3021
3409
|
disable_raw_mode()?;
|
|
3022
3410
|
redraw_input_box(
|
|
@@ -3025,16 +3413,39 @@ fn read_line_interactive(
|
|
|
3025
3413
|
placeholder,
|
|
3026
3414
|
model,
|
|
3027
3415
|
path_str,
|
|
3028
|
-
|
|
3029
|
-
|
|
3416
|
+
Some(&base),
|
|
3417
|
+
current_highlight,
|
|
3418
|
+
current_dir,
|
|
3030
3419
|
);
|
|
3031
3420
|
enable_raw_mode()?;
|
|
3032
3421
|
}
|
|
3033
3422
|
}
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
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();
|
|
3038
3449
|
|
|
3039
3450
|
disable_raw_mode()?;
|
|
3040
3451
|
redraw_input_box(
|
|
@@ -3043,16 +3454,30 @@ fn read_line_interactive(
|
|
|
3043
3454
|
placeholder,
|
|
3044
3455
|
model,
|
|
3045
3456
|
path_str,
|
|
3046
|
-
|
|
3047
|
-
|
|
3457
|
+
Some(&base),
|
|
3458
|
+
Some(new_idx),
|
|
3459
|
+
current_dir,
|
|
3048
3460
|
);
|
|
3049
3461
|
enable_raw_mode()?;
|
|
3050
3462
|
}
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
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();
|
|
3056
3481
|
|
|
3057
3482
|
disable_raw_mode()?;
|
|
3058
3483
|
redraw_input_box(
|
|
@@ -3061,209 +3486,171 @@ fn read_line_interactive(
|
|
|
3061
3486
|
placeholder,
|
|
3062
3487
|
model,
|
|
3063
3488
|
path_str,
|
|
3064
|
-
|
|
3065
|
-
|
|
3489
|
+
Some(&base),
|
|
3490
|
+
Some(new_idx),
|
|
3491
|
+
current_dir,
|
|
3066
3492
|
);
|
|
3067
3493
|
enable_raw_mode()?;
|
|
3068
3494
|
}
|
|
3069
3495
|
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
};
|
|
3079
|
-
|
|
3080
|
-
if base.starts_with('/') {
|
|
3081
|
-
let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
|
|
3082
|
-
.iter()
|
|
3083
|
-
.filter(|(cmd, _)| cmd.starts_with(&base))
|
|
3084
|
-
.collect();
|
|
3085
|
-
|
|
3086
|
-
if !matches.is_empty() {
|
|
3087
|
-
let idx = tab_index.unwrap_or(0) % matches.len();
|
|
3088
|
-
let completed = format!("{} ", matches[idx].0);
|
|
3089
|
-
input_chars = completed.chars().collect();
|
|
3090
|
-
cursor_pos = input_chars.len();
|
|
3091
|
-
|
|
3092
|
-
// Highlight currently completed item in suggestions
|
|
3093
|
-
let current_highlight = Some(idx);
|
|
3094
|
-
tab_index = Some(idx + 1);
|
|
3095
|
-
|
|
3096
|
-
disable_raw_mode()?;
|
|
3097
|
-
redraw_input_box(
|
|
3098
|
-
&input_chars,
|
|
3099
|
-
cursor_pos,
|
|
3100
|
-
placeholder,
|
|
3101
|
-
model,
|
|
3102
|
-
path_str,
|
|
3103
|
-
Some(&base),
|
|
3104
|
-
current_highlight,
|
|
3105
|
-
);
|
|
3106
|
-
enable_raw_mode()?;
|
|
3107
|
-
}
|
|
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
|
|
3108
3504
|
}
|
|
3109
|
-
}
|
|
3110
|
-
KeyCode::Down => {
|
|
3111
|
-
let base = match &tab_base_input {
|
|
3112
|
-
Some(b) => b.clone(),
|
|
3113
|
-
None => {
|
|
3114
|
-
let current_str: String = input_chars.iter().collect();
|
|
3115
|
-
tab_base_input = Some(current_str.clone());
|
|
3116
|
-
current_str
|
|
3117
|
-
}
|
|
3118
|
-
};
|
|
3505
|
+
};
|
|
3119
3506
|
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
model,
|
|
3142
|
-
path_str,
|
|
3143
|
-
Some(&base),
|
|
3144
|
-
Some(new_idx),
|
|
3145
|
-
);
|
|
3146
|
-
enable_raw_mode()?;
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
KeyCode::Up => {
|
|
3151
|
-
let base = match &tab_base_input {
|
|
3152
|
-
Some(b) => b.clone(),
|
|
3153
|
-
None => {
|
|
3154
|
-
let current_str: String = input_chars.iter().collect();
|
|
3155
|
-
tab_base_input = Some(current_str.clone());
|
|
3156
|
-
current_str
|
|
3157
|
-
}
|
|
3158
|
-
};
|
|
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();
|
|
3159
3528
|
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
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
|
|
3174
3558
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
)
|
|
3192
|
-
|
|
3193
|
-
|
|
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()?;
|
|
3194
3579
|
}
|
|
3195
3580
|
}
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
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;
|
|
3202
3587
|
}
|
|
3203
|
-
disable_raw_mode()?;
|
|
3204
|
-
redraw_input_box(
|
|
3205
|
-
&input_chars,
|
|
3206
|
-
cursor_pos,
|
|
3207
|
-
placeholder,
|
|
3208
|
-
model,
|
|
3209
|
-
path_str,
|
|
3210
|
-
None,
|
|
3211
|
-
None,
|
|
3212
|
-
);
|
|
3213
|
-
enable_raw_mode()?;
|
|
3214
3588
|
}
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
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;
|
|
3223
3609
|
}
|
|
3224
|
-
disable_raw_mode()?;
|
|
3225
|
-
redraw_input_box(
|
|
3226
|
-
&input_chars,
|
|
3227
|
-
cursor_pos,
|
|
3228
|
-
placeholder,
|
|
3229
|
-
model,
|
|
3230
|
-
path_str,
|
|
3231
|
-
None,
|
|
3232
|
-
None,
|
|
3233
|
-
);
|
|
3234
|
-
enable_raw_mode()?;
|
|
3235
3610
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
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();
|
|
3245
3633
|
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
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
|
+
}
|
|
3250
3638
|
|
|
3251
|
-
|
|
3252
|
-
|
|
3639
|
+
println!(" {BLUE}You ›{RESET} {}", expanded_str);
|
|
3640
|
+
let _ = io::stdout().flush();
|
|
3253
3641
|
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
}
|
|
3265
|
-
_ => {}
|
|
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;
|
|
3266
3652
|
}
|
|
3653
|
+
_ => {}
|
|
3267
3654
|
}
|
|
3268
3655
|
}
|
|
3269
3656
|
}
|
|
@@ -3303,18 +3690,18 @@ fn format_placeholders(s: &str) -> String {
|
|
|
3303
3690
|
while i < chars.len() {
|
|
3304
3691
|
if chars[i] == '[' && i + 12 < chars.len() {
|
|
3305
3692
|
let slice: String = chars[i..i + 13].iter().collect();
|
|
3306
|
-
if slice == "[Pasted text #"
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
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;
|
|
3318
3705
|
}
|
|
3319
3706
|
}
|
|
3320
3707
|
result.push(chars[i]);
|
|
@@ -3345,6 +3732,22 @@ fn active_model<'a>(provider: &str, config: &'a mint_core::MintConfig) -> &'a st
|
|
|
3345
3732
|
"huggingface" => &config.hf_model,
|
|
3346
3733
|
"local_openai" => &config.local_model_name,
|
|
3347
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
|
+
}
|
|
3348
3751
|
_ => &config.gemini_model,
|
|
3349
3752
|
}
|
|
3350
3753
|
}
|