@pheem49/mint 1.8.1 → 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.lock +3 -3
- package/Cargo.toml +1 -1
- package/README.md +0 -17
- package/Release_Note.md +137 -58
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +150 -45
- package/crates/mint-cli/src/image.rs +8 -8
- package/crates/mint-cli/src/main.rs +929 -412
- package/crates/mint-cli/src/onboard.rs +108 -108
- package/crates/mint-cli/src/setup.rs +143 -143
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +108 -52
- package/crates/mint-core/src/browser.rs +56 -1
- package/crates/mint-core/src/channels.rs +1 -0
- package/crates/mint-core/src/chat.rs +126 -8
- package/crates/mint-core/src/code_tools.rs +18 -20
- package/crates/mint-core/src/config.rs +245 -17
- package/crates/mint-core/src/image_gen.rs +5 -5
- package/crates/mint-core/src/lib.rs +16 -2
- package/crates/mint-core/src/memory.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +260 -72
- 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/crates/mint-core/src/tts.rs +3 -1
- package/flatpak/com.pheem49.mint.desktop +9 -0
- package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
- package/flatpak/com.pheem49.mint.yaml +52 -0
- package/package.json +1 -1
- package/shared-modules/.github/workflows/ci.yml +120 -0
- package/shared-modules/.github/workflows/update.yaml +28 -0
- package/shared-modules/CODEOWNERS +23 -0
- package/shared-modules/README.md +79 -0
- package/shared-modules/SDL/README.md +3 -0
- package/shared-modules/SDL/SDL-1.2.15.json +51 -0
- package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
- package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
- package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
- package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
- package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
- package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
- package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
- package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
- package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
- package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
- package/shared-modules/SDL/sdl12-compat.json +22 -0
- package/shared-modules/SDL2/SDL2_image.json +27 -0
- package/shared-modules/SDL2/SDL2_ttf.json +29 -0
- package/shared-modules/cld2/CMakeLists.txt +157 -0
- package/shared-modules/cld2/cld2.json +24 -0
- package/shared-modules/clutter/clutter.json +72 -0
- package/shared-modules/dbus-glib/dbus-glib.json +29 -0
- package/shared-modules/glew/glew.json +36 -0
- package/shared-modules/glu/glu-9.json +12 -0
- package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
- package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
- package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
- package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
- package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
- package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
- package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
- package/shared-modules/gtk2/gtk2.json +120 -0
- package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
- package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
- package/shared-modules/gtk2/readme.md +10 -0
- package/shared-modules/intltool/intltool-0.51.json +20 -0
- package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
- package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
- package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
- package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
- package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
- package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
- package/shared-modules/lame/lame-3.99.5.json +32 -0
- package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
- package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
- package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
- package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
- package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
- package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
- package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
- package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
- package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
- package/shared-modules/libcanberra/libcanberra.json +44 -0
- package/shared-modules/libmad/libmad.json +19 -0
- package/shared-modules/libsecret/libsecret.json +23 -0
- package/shared-modules/libsoup/libsoup-2.4.json +14 -0
- package/shared-modules/libusb/libusb.json +27 -0
- package/shared-modules/linux-audio/dssi.json +31 -0
- package/shared-modules/linux-audio/fftw3f-static.json +44 -0
- package/shared-modules/linux-audio/fftw3f.json +42 -0
- package/shared-modules/linux-audio/fluidsynth2-static.json +36 -0
- package/shared-modules/linux-audio/fluidsynth2.json +25 -0
- package/shared-modules/linux-audio/ladspa.json +26 -0
- package/shared-modules/linux-audio/lash.json +35 -0
- package/shared-modules/linux-audio/libinstpatch.json +16 -0
- package/shared-modules/linux-audio/liblo-static.json +22 -0
- package/shared-modules/linux-audio/liblo.json +17 -0
- package/shared-modules/linux-audio/lilv-static.json +94 -0
- package/shared-modules/linux-audio/lilv.json +89 -0
- package/shared-modules/linux-audio/lrdf.json +57 -0
- package/shared-modules/linux-audio/lv2.json +20 -0
- package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
- package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
- package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
- package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
- package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
- package/shared-modules/linux-audio/stk.json +28 -0
- package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
- package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
- package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
- package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
- package/shared-modules/lua5.3/lua.pc.in +8 -0
- package/shared-modules/lua5.3/luaconf.patch +47 -0
- package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
- package/shared-modules/lua5.4/lua-5.4.json +41 -0
- package/shared-modules/lua5.4/lua.pc.in +8 -0
- package/shared-modules/lua5.4/luaconf.patch +34 -0
- package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
- package/shared-modules/luajit/luajit.json +30 -0
- package/shared-modules/lzo/lzo.json +27 -0
- package/shared-modules/mac/mac.json +18 -0
- package/shared-modules/physfs/physfs.json +26 -0
- package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
- package/shared-modules/pygame/cython-fixes.patch +34 -0
- package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
- package/shared-modules/pygame/glib.patch +25 -0
- package/shared-modules/pygame/pygame1-python2.json +157 -0
- package/shared-modules/pygtk/pygtk-c99.patch +15 -0
- package/shared-modules/pygtk/pygtk.json +117 -0
- package/shared-modules/pygtk/pygtk.patch +39 -0
- package/shared-modules/python2.7/python-2.7.json +48 -0
- package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
- package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
- package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
- package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
- package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
- package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
- package/shared-modules/update-checker.sh +50 -0
- package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
- package/shared-modules/vorbisgain/0002-errno.patch +33 -0
- package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
- package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
- package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
- package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
- package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
- package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
- package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
- package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
- package/shared-modules/vorbisgain/README.md +23 -0
- package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
- package/src/renderer/shared/agentProgress.ts +145 -0
- package/src/renderer/shared/calculator.ts +52 -0
- package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
- package/src/renderer/shared/components/AgentActivityTable.tsx +85 -0
- package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
- package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
- package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
- package/src/renderer/shared/constants/models.ts +91 -0
- package/src/renderer/shared/platform.ts +82 -0
- package/src/renderer/shared/types.ts +140 -0
- package/src/renderer/shared/utils/agentActivity.ts +187 -0
- package/src/renderer/shared/utils/approval.ts +1 -0
- package/src/renderer/shared/utils/approval.tsx +64 -0
- package/src/renderer/shared/utils/fileIcons.ts +135 -0
- package/src/renderer/shared/utils/markdown.tsx +347 -0
- package/src/renderer/shared/utils/providers.ts +36 -0
- package/src/renderer/shared/utils/speech.ts +216 -0
- package/src/renderer/shared/utils/ui.ts +147 -0
- package/src/renderer/src/agentProgress.ts +11 -39
- package/src/renderer/src/calculator.ts +3 -46
- package/src/renderer/src/components/ChatPanel.tsx +219 -1308
- package/src/renderer/src/components/MintDashboard.tsx +67 -170
- package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
- package/src/renderer/src/components/Settings/AgentsTab.tsx +381 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src/components/Settings/GeneralTab.tsx +256 -2
- package/src/renderer/src/components/SettingsWindow.tsx +120 -3
- package/src/renderer/src/components/WorkspacePanel.tsx +7 -125
- package/src/renderer/src/css/styles.css +100 -0
- package/src/renderer/src/tauri.ts +128 -143
- package/src/renderer/src-web/agentProgress.ts +11 -39
- package/src/renderer/src-web/calculator.ts +3 -46
- package/src/renderer/src-web/components/ChatPanel.tsx +219 -1306
- package/src/renderer/src-web/components/MintDashboard.tsx +42 -170
- package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +381 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +254 -3
- package/src/renderer/src-web/components/SettingsWindow.tsx +127 -3
- package/src/renderer/src-web/css/styles.css +100 -0
- package/src/renderer/src-web/tauri.ts +180 -102
- package/src-tauri/src/desktop.rs +18 -16
- package/src-tauri/src/discord_rpc.rs +1 -1
- package/src-tauri/src/lib.rs +168 -40
- package/src-tauri/src/webhooks.rs +1 -0
- package/src-tauri/src/workflows.rs +1 -0
- 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();
|
|
@@ -803,6 +800,7 @@ async fn main() -> Result<()> {
|
|
|
803
800
|
audio_data_uri: None,
|
|
804
801
|
document_attachment: None,
|
|
805
802
|
workspace_path: None,
|
|
803
|
+
agent_id: None,
|
|
806
804
|
},
|
|
807
805
|
)
|
|
808
806
|
.await?;
|
|
@@ -1152,10 +1150,10 @@ async fn main() -> Result<()> {
|
|
|
1152
1150
|
),
|
|
1153
1151
|
}
|
|
1154
1152
|
}
|
|
1155
|
-
if let Some(desc) = &result.description
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1153
|
+
if let Some(desc) = &result.description
|
|
1154
|
+
&& !desc.is_empty()
|
|
1155
|
+
{
|
|
1156
|
+
println!("\n{DIM}{desc}{RESET}");
|
|
1159
1157
|
}
|
|
1160
1158
|
}
|
|
1161
1159
|
Err(e) => eprintln!("{ERROR}Failed to save images: {e}{RESET}"),
|
|
@@ -1200,16 +1198,16 @@ async fn launch_mint_target(target: String) -> Result<()> {
|
|
|
1200
1198
|
path = p.parent();
|
|
1201
1199
|
}
|
|
1202
1200
|
}
|
|
1203
|
-
if found.is_none()
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
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;
|
|
1212
1209
|
}
|
|
1210
|
+
path = p.parent();
|
|
1213
1211
|
}
|
|
1214
1212
|
}
|
|
1215
1213
|
found.ok_or_else(|| {
|
|
@@ -1218,7 +1216,7 @@ async fn launch_mint_target(target: String) -> Result<()> {
|
|
|
1218
1216
|
};
|
|
1219
1217
|
std::process::Command::new("npm")
|
|
1220
1218
|
.current_dir(&project_root)
|
|
1221
|
-
.args(
|
|
1219
|
+
.args(["run", "dev:web"])
|
|
1222
1220
|
.stdout(std::process::Stdio::null())
|
|
1223
1221
|
.stderr(std::process::Stdio::null())
|
|
1224
1222
|
.spawn()
|
|
@@ -1401,7 +1399,7 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1401
1399
|
rendered.push_str(&draw_border("┌", "┬", "┐", "─", &col_widths));
|
|
1402
1400
|
|
|
1403
1401
|
if let Some(ref header) = header_row {
|
|
1404
|
-
rendered.
|
|
1402
|
+
rendered.push('│');
|
|
1405
1403
|
for (i, col) in header.iter().enumerate() {
|
|
1406
1404
|
let width = unicode_width(col);
|
|
1407
1405
|
let padding = col_widths[i] - width;
|
|
@@ -1410,20 +1408,20 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1410
1408
|
col,
|
|
1411
1409
|
" ".repeat(padding + 1)
|
|
1412
1410
|
));
|
|
1413
|
-
rendered.
|
|
1411
|
+
rendered.push('│');
|
|
1414
1412
|
}
|
|
1415
1413
|
rendered.push('\n');
|
|
1416
1414
|
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1417
1415
|
}
|
|
1418
1416
|
|
|
1419
1417
|
for (r_idx, row) in data_rows.iter().enumerate() {
|
|
1420
|
-
rendered.
|
|
1418
|
+
rendered.push('│');
|
|
1421
1419
|
for i in 0..num_cols {
|
|
1422
1420
|
let col_val = row.get(i).cloned().unwrap_or_default();
|
|
1423
1421
|
let width = unicode_width(&col_val);
|
|
1424
1422
|
let padding = col_widths[i] - width;
|
|
1425
1423
|
rendered.push_str(&format!(" {}{}", col_val, " ".repeat(padding + 1)));
|
|
1426
|
-
rendered.
|
|
1424
|
+
rendered.push('│');
|
|
1427
1425
|
}
|
|
1428
1426
|
rendered.push('\n');
|
|
1429
1427
|
|
|
@@ -1432,7 +1430,7 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1432
1430
|
}
|
|
1433
1431
|
}
|
|
1434
1432
|
|
|
1435
|
-
rendered.push_str(
|
|
1433
|
+
rendered.push_str(draw_border("└", "┴", "┘", "─", &col_widths).trim_end());
|
|
1436
1434
|
rendered
|
|
1437
1435
|
}
|
|
1438
1436
|
|
|
@@ -1746,6 +1744,10 @@ async fn handle_slash_command(
|
|
|
1746
1744
|
("/paste [prompt]", "Attach image from clipboard"),
|
|
1747
1745
|
("Ctrl+V", "Paste clipboard image as [Image #1]"),
|
|
1748
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
|
+
),
|
|
1749
1751
|
(
|
|
1750
1752
|
"/plugins <name> [prompt]",
|
|
1751
1753
|
"Generate a skill.md file for a plugin/skill",
|
|
@@ -1761,8 +1763,12 @@ async fn handle_slash_command(
|
|
|
1761
1763
|
("/mcp list", "List configured MCP servers"),
|
|
1762
1764
|
("/mcp allow <server> <tool>", "Allow an MCP tool"),
|
|
1763
1765
|
("/stats", "Show session statistics"),
|
|
1764
|
-
("/exit
|
|
1766
|
+
("/exit", "Exit Mint"),
|
|
1765
1767
|
("/code <task>", "Run in code-agent mode"),
|
|
1768
|
+
(
|
|
1769
|
+
"/multi-agent [on|off]",
|
|
1770
|
+
"Show or toggle Multi-Agent Collaboration system",
|
|
1771
|
+
),
|
|
1766
1772
|
];
|
|
1767
1773
|
for (cmd_name, desc) in &commands {
|
|
1768
1774
|
println!(" {MINT}{:<30}{RESET} {DIM}{}{RESET}", cmd_name, desc);
|
|
@@ -1803,13 +1809,85 @@ async fn handle_slash_command(
|
|
|
1803
1809
|
println!();
|
|
1804
1810
|
} else {
|
|
1805
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";
|
|
1806
1847
|
match mint_core::save_config(&session.config) {
|
|
1807
1848
|
Ok(()) => println!(
|
|
1808
|
-
"{DIM}
|
|
1809
|
-
session.config.
|
|
1849
|
+
"{DIM}Multi-Agent collaboration set to: {}{RESET}\n",
|
|
1850
|
+
if session.config.enable_agent_collaboration {
|
|
1851
|
+
"Enabled"
|
|
1852
|
+
} else {
|
|
1853
|
+
"Disabled"
|
|
1854
|
+
}
|
|
1810
1855
|
),
|
|
1811
|
-
Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
|
|
1856
|
+
Err(error) => println!("{ERROR}Config error:{RESET} {error}\n"),
|
|
1812
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!();
|
|
1813
1891
|
}
|
|
1814
1892
|
Some(SlashResult::Handled)
|
|
1815
1893
|
}
|
|
@@ -2024,6 +2102,73 @@ async fn handle_slash_command(
|
|
|
2024
2102
|
Some(SlashResult::Handled)
|
|
2025
2103
|
}
|
|
2026
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
|
+
|
|
2027
2172
|
"/memory" => {
|
|
2028
2173
|
let memory = match MemoryStore::open_default() {
|
|
2029
2174
|
Ok(m) => m,
|
|
@@ -2222,44 +2367,40 @@ async fn handle_slash_command(
|
|
|
2222
2367
|
|
|
2223
2368
|
// Scan local workspace skills directory
|
|
2224
2369
|
let local_skills_dir = session.current_dir.join("skills");
|
|
2225
|
-
if local_skills_dir.is_dir()
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
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));
|
|
2244
2385
|
}
|
|
2245
2386
|
}
|
|
2387
|
+
}
|
|
2246
2388
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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();
|
|
2252
2394
|
|
|
2253
|
-
|
|
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 {
|
|
2254
2400
|
println!(
|
|
2255
|
-
"
|
|
2401
|
+
" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
|
|
2402
|
+
file, file, file
|
|
2256
2403
|
);
|
|
2257
|
-
for file in inactive_files {
|
|
2258
|
-
println!(
|
|
2259
|
-
" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
|
|
2260
|
-
file, file, file
|
|
2261
|
-
);
|
|
2262
|
-
}
|
|
2263
2404
|
}
|
|
2264
2405
|
}
|
|
2265
2406
|
}
|
|
@@ -2318,7 +2459,8 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2318
2459
|
now.format("%M")
|
|
2319
2460
|
);
|
|
2320
2461
|
let version = env!("CARGO_PKG_VERSION");
|
|
2321
|
-
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);
|
|
2322
2464
|
let line2_text = format!("{} • {}", date_time, model);
|
|
2323
2465
|
|
|
2324
2466
|
let len1 = line1_text.chars().count();
|
|
@@ -2340,7 +2482,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2340
2482
|
println!(
|
|
2341
2483
|
"{MINT}| \\/ (_)_ __ | |_ / __| | |_ _|{RESET} {DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
|
|
2342
2484
|
version,
|
|
2343
|
-
|
|
2485
|
+
clean_provider_name,
|
|
2344
2486
|
" ".repeat(content_width - len1)
|
|
2345
2487
|
);
|
|
2346
2488
|
println!(
|
|
@@ -2357,7 +2499,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2357
2499
|
println!(
|
|
2358
2500
|
"{DIM}│{RESET} {MINT}[Mint]{RESET} v{} | Active AI: {}{} {DIM}│{RESET}",
|
|
2359
2501
|
version,
|
|
2360
|
-
|
|
2502
|
+
clean_provider_name,
|
|
2361
2503
|
" ".repeat(content_width - len1)
|
|
2362
2504
|
);
|
|
2363
2505
|
println!(
|
|
@@ -2397,11 +2539,11 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2397
2539
|
loop {
|
|
2398
2540
|
if let Some(handle) = update_handle.take() {
|
|
2399
2541
|
if handle.is_finished() {
|
|
2400
|
-
if let Ok(Some((current, latest))) = handle.await
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2542
|
+
if let Ok(Some((current, latest))) = handle.await
|
|
2543
|
+
&& !printed_update
|
|
2544
|
+
{
|
|
2545
|
+
updater::print_update_notice(¤t, &latest);
|
|
2546
|
+
printed_update = true;
|
|
2405
2547
|
}
|
|
2406
2548
|
} else {
|
|
2407
2549
|
update_handle = Some(handle);
|
|
@@ -2411,9 +2553,12 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2411
2553
|
let path_str = format_path_with_tilde(&session.current_dir);
|
|
2412
2554
|
let model_str = active_model(&session.config.ai_provider, &session.config).to_owned();
|
|
2413
2555
|
|
|
2414
|
-
if let Some(input) =
|
|
2415
|
-
|
|
2416
|
-
|
|
2556
|
+
if let Some(input) = read_line_interactive(
|
|
2557
|
+
&session.config.ai_provider,
|
|
2558
|
+
&model_str,
|
|
2559
|
+
&path_str,
|
|
2560
|
+
&session.current_dir,
|
|
2561
|
+
)? {
|
|
2417
2562
|
if let Some(uri) = input.pasted_image {
|
|
2418
2563
|
if let Some(ref mut current) = session.pending_image {
|
|
2419
2564
|
current.push(' ');
|
|
@@ -2427,6 +2572,72 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2427
2572
|
continue;
|
|
2428
2573
|
}
|
|
2429
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
|
+
|
|
2430
2641
|
// Run slash-command router
|
|
2431
2642
|
match handle_slash_command(&mut session, &query_str).await {
|
|
2432
2643
|
Some(SlashResult::Handled) => continue,
|
|
@@ -2503,8 +2714,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2503
2714
|
print!(" {MINT}Mint:{RESET} {DIM}Thinking...{RESET}");
|
|
2504
2715
|
let _ = io::stdout().flush();
|
|
2505
2716
|
|
|
2506
|
-
let mut system_instruction =
|
|
2507
|
-
"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 \"คุณ\", \"ค่ะ\", \"นะคะ\"). \
|
|
2508
2718
|
You are running inside the Mint CLI interactive chat. \
|
|
2509
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: \
|
|
2510
2720
|
`[ACTION: <command> <arguments>]` \
|
|
@@ -2515,15 +2725,14 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2515
2725
|
- `[ACTION: read-folder <path>]` to list files/folders in a directory. \
|
|
2516
2726
|
- `[ACTION: run-shell <command>]` to run a non-destructive local shell command after approval. \
|
|
2517
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. \
|
|
2518
|
-
Write the action block on a single line at the very end of your response."
|
|
2519
|
-
)
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
}
|
|
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
|
+
));
|
|
2527
2736
|
}
|
|
2528
2737
|
|
|
2529
2738
|
let image_uri = session.pending_image.take();
|
|
@@ -2542,6 +2751,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2542
2751
|
audio_data_uri: None,
|
|
2543
2752
|
document_attachment: None,
|
|
2544
2753
|
workspace_path: None,
|
|
2754
|
+
agent_id: None,
|
|
2545
2755
|
},
|
|
2546
2756
|
|chunk| {
|
|
2547
2757
|
if first_chunk {
|
|
@@ -2621,9 +2831,10 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
2621
2831
|
|
|
2622
2832
|
fn print_exit_message(session: &InteractiveSession) {
|
|
2623
2833
|
println!("\n{MINT}──────────────── Mint session closed ────────────────{RESET}");
|
|
2834
|
+
let clean_provider = format_provider_display_name(&session.config.ai_provider, &session.config);
|
|
2624
2835
|
println!(
|
|
2625
2836
|
"{DIM}Provider:{RESET} {} {DIM}• Model:{RESET} {}",
|
|
2626
|
-
|
|
2837
|
+
clean_provider,
|
|
2627
2838
|
active_model(&session.config.ai_provider, &session.config)
|
|
2628
2839
|
);
|
|
2629
2840
|
println!(
|
|
@@ -2634,52 +2845,140 @@ fn print_exit_message(session: &InteractiveSession) {
|
|
|
2634
2845
|
println!("{MINT}See you next time.{RESET}\n");
|
|
2635
2846
|
}
|
|
2636
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
|
+
|
|
2637
2878
|
const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
|
|
2638
|
-
("/
|
|
2879
|
+
("/cd", "Change active workspace directory"),
|
|
2880
|
+
("/clear", "Clear conversation history"),
|
|
2881
|
+
("/code", "Run in code-agent mode"),
|
|
2882
|
+
("/exit", "Exit Mint CLI"),
|
|
2639
2883
|
("/fast", "Toggle fast mode (hide thinking traces)"),
|
|
2640
|
-
("/
|
|
2884
|
+
("/help", "Show help menu"),
|
|
2885
|
+
("/image", "Attach image from disk"),
|
|
2641
2886
|
(
|
|
2642
2887
|
"/image-provider",
|
|
2643
2888
|
"List image gen providers or switch default provider",
|
|
2644
2889
|
),
|
|
2645
|
-
("/clear", "Clear conversation history"),
|
|
2646
|
-
("/cd", "Change active workspace directory"),
|
|
2647
|
-
("/image", "Attach image from disk"),
|
|
2648
|
-
("/paste", "Attach image from clipboard"),
|
|
2649
2890
|
("/learn", "Import persistent skill/instruction"),
|
|
2650
|
-
("/plugins", "List or generate plugins/skills"),
|
|
2651
|
-
("/memory", "Manage long-term memory store"),
|
|
2652
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"),
|
|
2653
2901
|
("/stats", "Show session statistics"),
|
|
2654
|
-
("/exit", "Exit Mint CLI"),
|
|
2655
|
-
("/quit", "Exit Mint CLI"),
|
|
2656
|
-
("/code", "Run in code-agent mode"),
|
|
2657
2902
|
];
|
|
2658
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
|
+
|
|
2659
2941
|
fn draw_input_box(
|
|
2660
|
-
|
|
2942
|
+
input_chars: &[char],
|
|
2943
|
+
cursor_pos: usize,
|
|
2661
2944
|
placeholder: &str,
|
|
2662
2945
|
model: &str,
|
|
2663
2946
|
path_str: &str,
|
|
2664
2947
|
tab_base_input: Option<&str>,
|
|
2665
2948
|
tab_index: Option<usize>,
|
|
2666
|
-
|
|
2949
|
+
current_dir: &Path,
|
|
2950
|
+
) -> (usize, usize) {
|
|
2667
2951
|
let (term_width, _) = crossterm::terminal::size().unwrap_or((80, 24));
|
|
2668
2952
|
let width = term_width as usize;
|
|
2669
2953
|
let prefix = "› ";
|
|
2670
2954
|
let input_width = width.saturating_sub(2);
|
|
2671
2955
|
let content_max_len = input_width.saturating_sub(prefix.chars().count());
|
|
2672
2956
|
|
|
2673
|
-
let
|
|
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() {
|
|
2674
2973
|
format!("{DIM}{}\x1b[39m", placeholder)
|
|
2675
2974
|
} else {
|
|
2676
|
-
format_placeholders(
|
|
2975
|
+
format_placeholders(&displayed_input)
|
|
2677
2976
|
};
|
|
2678
2977
|
|
|
2679
|
-
let visible_len = if
|
|
2978
|
+
let visible_len = if displayed_input.is_empty() {
|
|
2680
2979
|
placeholder.chars().count()
|
|
2681
2980
|
} else {
|
|
2682
|
-
string_visual_width(
|
|
2981
|
+
string_visual_width(&displayed_input)
|
|
2683
2982
|
};
|
|
2684
2983
|
|
|
2685
2984
|
let pad_len = content_max_len.saturating_sub(visible_len);
|
|
@@ -2707,7 +3006,8 @@ fn draw_input_box(
|
|
|
2707
3006
|
);
|
|
2708
3007
|
|
|
2709
3008
|
// Compute and draw suggestions
|
|
2710
|
-
let
|
|
3009
|
+
let raw_input: String = input_chars.iter().collect();
|
|
3010
|
+
let search_query = tab_base_input.unwrap_or(&raw_input);
|
|
2711
3011
|
let mut match_count = 0;
|
|
2712
3012
|
if search_query.starts_with('/') {
|
|
2713
3013
|
let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
|
|
@@ -2716,11 +3016,22 @@ fn draw_input_box(
|
|
|
2716
3016
|
.collect();
|
|
2717
3017
|
|
|
2718
3018
|
if !matches.is_empty() {
|
|
2719
|
-
|
|
2720
|
-
println!();
|
|
2721
|
-
println!(" {BLUE}Suggestions{RESET}");
|
|
3019
|
+
let total_pages = matches.len().div_ceil(5);
|
|
2722
3020
|
let highlight_idx = tab_index.map(|idx| idx % matches.len());
|
|
2723
|
-
|
|
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];
|
|
2724
3035
|
if Some(i) == highlight_idx {
|
|
2725
3036
|
println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
|
|
2726
3037
|
} else {
|
|
@@ -2728,14 +3039,86 @@ fn draw_input_box(
|
|
|
2728
3039
|
}
|
|
2729
3040
|
}
|
|
2730
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
|
+
}
|
|
2731
3098
|
}
|
|
2732
3099
|
|
|
2733
3100
|
let _ = io::stdout().flush();
|
|
2734
|
-
match_count
|
|
3101
|
+
(match_count, start_idx)
|
|
2735
3102
|
}
|
|
2736
3103
|
|
|
2737
3104
|
fn input_cursor_column(input_chars: &[char], cursor_pos: usize) -> usize {
|
|
2738
|
-
let
|
|
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]
|
|
2739
3122
|
.iter()
|
|
2740
3123
|
.copied()
|
|
2741
3124
|
.map(char_visual_width)
|
|
@@ -2764,16 +3147,18 @@ fn redraw_input_box(
|
|
|
2764
3147
|
path_str: &str,
|
|
2765
3148
|
tab_base_input: Option<&str>,
|
|
2766
3149
|
tab_index: Option<usize>,
|
|
3150
|
+
current_dir: &Path,
|
|
2767
3151
|
) {
|
|
2768
3152
|
clear_input_box();
|
|
2769
|
-
let
|
|
2770
|
-
|
|
2771
|
-
|
|
3153
|
+
let (match_count, _) = draw_input_box(
|
|
3154
|
+
input_chars,
|
|
3155
|
+
cursor_pos,
|
|
2772
3156
|
placeholder,
|
|
2773
3157
|
model,
|
|
2774
3158
|
path_str,
|
|
2775
3159
|
tab_base_input,
|
|
2776
3160
|
tab_index,
|
|
3161
|
+
current_dir,
|
|
2777
3162
|
);
|
|
2778
3163
|
position_input_cursor(input_chars, cursor_pos, match_count);
|
|
2779
3164
|
let _ = io::stdout().flush();
|
|
@@ -2783,6 +3168,7 @@ fn read_line_interactive(
|
|
|
2783
3168
|
_provider: &str,
|
|
2784
3169
|
model: &str,
|
|
2785
3170
|
path_str: &str,
|
|
3171
|
+
current_dir: &Path,
|
|
2786
3172
|
) -> io::Result<Option<InteractiveInput>> {
|
|
2787
3173
|
use crossterm::event::{self, Event, KeyCode};
|
|
2788
3174
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
|
@@ -2799,7 +3185,16 @@ fn read_line_interactive(
|
|
|
2799
3185
|
let mut tab_base_input: Option<String> = None;
|
|
2800
3186
|
let mut tab_index: Option<usize> = None;
|
|
2801
3187
|
|
|
2802
|
-
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
|
+
);
|
|
2803
3198
|
position_input_cursor(&input_chars, cursor_pos, match_count);
|
|
2804
3199
|
let _ = io::stdout().flush();
|
|
2805
3200
|
|
|
@@ -2839,70 +3234,177 @@ fn read_line_interactive(
|
|
|
2839
3234
|
path_str,
|
|
2840
3235
|
None,
|
|
2841
3236
|
None,
|
|
3237
|
+
current_dir,
|
|
2842
3238
|
);
|
|
2843
3239
|
enable_raw_mode()?;
|
|
2844
3240
|
last_paste_time = Some(std::time::Instant::now());
|
|
2845
3241
|
continue;
|
|
2846
3242
|
}
|
|
2847
|
-
if let Event::Key(key_event) = ev
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
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
|
|
2851
3292
|
.modifiers
|
|
2852
|
-
.contains(crossterm::event::KeyModifiers::CONTROL)
|
|
2853
|
-
|
|
2854
|
-
|
|
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
|
+
|
|
2855
3322
|
disable_raw_mode()?;
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
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
|
+
);
|
|
2859
3333
|
enable_raw_mode()?;
|
|
2860
3334
|
}
|
|
3335
|
+
KeyCode::Backspace if cursor_pos > 0 => {
|
|
3336
|
+
cursor_pos -= 1;
|
|
3337
|
+
input_chars.remove(cursor_pos);
|
|
2861
3338
|
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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()?;
|
|
2869
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);
|
|
2870
3377
|
|
|
2871
|
-
match key_event.code {
|
|
2872
|
-
KeyCode::Char('d')
|
|
2873
|
-
if key_event
|
|
2874
|
-
.modifiers
|
|
2875
|
-
.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
|
2876
|
-
{
|
|
2877
|
-
if ctrl_d_pressed {
|
|
2878
|
-
disable_raw_mode()?;
|
|
2879
|
-
clear_input_box();
|
|
2880
|
-
let _ = io::stdout().flush();
|
|
2881
|
-
break None;
|
|
2882
|
-
} else {
|
|
2883
|
-
ctrl_d_pressed = true;
|
|
2884
3378
|
disable_raw_mode()?;
|
|
2885
|
-
|
|
2886
|
-
|
|
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,
|
|
2887
3388
|
);
|
|
2888
|
-
print!("\x1b[{}G", input_cursor_column(&input_chars, cursor_pos));
|
|
2889
|
-
let _ = io::stdout().flush();
|
|
2890
3389
|
enable_raw_mode()?;
|
|
2891
3390
|
}
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
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);
|
|
2906
3408
|
|
|
2907
3409
|
disable_raw_mode()?;
|
|
2908
3410
|
redraw_input_box(
|
|
@@ -2911,21 +3413,39 @@ fn read_line_interactive(
|
|
|
2911
3413
|
placeholder,
|
|
2912
3414
|
model,
|
|
2913
3415
|
path_str,
|
|
2914
|
-
|
|
2915
|
-
|
|
3416
|
+
Some(&base),
|
|
3417
|
+
current_highlight,
|
|
3418
|
+
current_dir,
|
|
2916
3419
|
);
|
|
2917
3420
|
enable_raw_mode()?;
|
|
2918
3421
|
}
|
|
2919
3422
|
}
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
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
|
+
};
|
|
2925
3433
|
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
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();
|
|
2929
3449
|
|
|
2930
3450
|
disable_raw_mode()?;
|
|
2931
3451
|
redraw_input_box(
|
|
@@ -2934,16 +3454,30 @@ fn read_line_interactive(
|
|
|
2934
3454
|
placeholder,
|
|
2935
3455
|
model,
|
|
2936
3456
|
path_str,
|
|
2937
|
-
|
|
2938
|
-
|
|
3457
|
+
Some(&base),
|
|
3458
|
+
Some(new_idx),
|
|
3459
|
+
current_dir,
|
|
2939
3460
|
);
|
|
2940
3461
|
enable_raw_mode()?;
|
|
2941
3462
|
}
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
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();
|
|
2947
3481
|
|
|
2948
3482
|
disable_raw_mode()?;
|
|
2949
3483
|
redraw_input_box(
|
|
@@ -2952,205 +3486,171 @@ fn read_line_interactive(
|
|
|
2952
3486
|
placeholder,
|
|
2953
3487
|
model,
|
|
2954
3488
|
path_str,
|
|
2955
|
-
|
|
2956
|
-
|
|
3489
|
+
Some(&base),
|
|
3490
|
+
Some(new_idx),
|
|
3491
|
+
current_dir,
|
|
2957
3492
|
);
|
|
2958
3493
|
enable_raw_mode()?;
|
|
2959
3494
|
}
|
|
2960
3495
|
}
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
};
|
|
2970
|
-
|
|
2971
|
-
if base.starts_with('/') {
|
|
2972
|
-
let matches: Vec<_> = AUTOCOMPLETE_COMMANDS
|
|
2973
|
-
.iter()
|
|
2974
|
-
.filter(|(cmd, _)| cmd.starts_with(&base))
|
|
2975
|
-
.collect();
|
|
2976
|
-
|
|
2977
|
-
if !matches.is_empty() {
|
|
2978
|
-
let idx = tab_index.unwrap_or(0) % matches.len();
|
|
2979
|
-
let completed = format!("{} ", matches[idx].0);
|
|
2980
|
-
input_chars = completed.chars().collect();
|
|
2981
|
-
cursor_pos = input_chars.len();
|
|
2982
|
-
|
|
2983
|
-
// Highlight currently completed item in suggestions
|
|
2984
|
-
let current_highlight = Some(idx);
|
|
2985
|
-
tab_index = Some(idx + 1);
|
|
2986
|
-
|
|
2987
|
-
disable_raw_mode()?;
|
|
2988
|
-
redraw_input_box(
|
|
2989
|
-
&input_chars,
|
|
2990
|
-
cursor_pos,
|
|
2991
|
-
placeholder,
|
|
2992
|
-
model,
|
|
2993
|
-
path_str,
|
|
2994
|
-
Some(&base),
|
|
2995
|
-
current_highlight,
|
|
2996
|
-
);
|
|
2997
|
-
enable_raw_mode()?;
|
|
2998
|
-
}
|
|
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
|
|
2999
3504
|
}
|
|
3000
|
-
}
|
|
3001
|
-
KeyCode::Down => {
|
|
3002
|
-
let base = match &tab_base_input {
|
|
3003
|
-
Some(b) => b.clone(),
|
|
3004
|
-
None => {
|
|
3005
|
-
let current_str: String = input_chars.iter().collect();
|
|
3006
|
-
tab_base_input = Some(current_str.clone());
|
|
3007
|
-
current_str
|
|
3008
|
-
}
|
|
3009
|
-
};
|
|
3505
|
+
};
|
|
3010
3506
|
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
model,
|
|
3033
|
-
path_str,
|
|
3034
|
-
Some(&base),
|
|
3035
|
-
Some(new_idx),
|
|
3036
|
-
);
|
|
3037
|
-
enable_raw_mode()?;
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
KeyCode::Up => {
|
|
3042
|
-
let base = match &tab_base_input {
|
|
3043
|
-
Some(b) => b.clone(),
|
|
3044
|
-
None => {
|
|
3045
|
-
let current_str: String = input_chars.iter().collect();
|
|
3046
|
-
tab_base_input = Some(current_str.clone());
|
|
3047
|
-
current_str
|
|
3048
|
-
}
|
|
3049
|
-
};
|
|
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();
|
|
3050
3528
|
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
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
|
|
3065
3558
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
)
|
|
3083
|
-
|
|
3084
|
-
|
|
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()?;
|
|
3085
3579
|
}
|
|
3086
3580
|
}
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
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;
|
|
3093
3587
|
}
|
|
3094
|
-
disable_raw_mode()?;
|
|
3095
|
-
let visual_cursor_pos: usize = input_chars[..cursor_pos]
|
|
3096
|
-
.iter()
|
|
3097
|
-
.copied()
|
|
3098
|
-
.map(char_visual_width)
|
|
3099
|
-
.sum();
|
|
3100
|
-
print!("\x1b[{}G", 4 + visual_cursor_pos);
|
|
3101
|
-
let _ = io::stdout().flush();
|
|
3102
|
-
enable_raw_mode()?;
|
|
3103
3588
|
}
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
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;
|
|
3112
3609
|
}
|
|
3113
|
-
disable_raw_mode()?;
|
|
3114
|
-
let visual_cursor_pos: usize = input_chars[..cursor_pos]
|
|
3115
|
-
.iter()
|
|
3116
|
-
.copied()
|
|
3117
|
-
.map(char_visual_width)
|
|
3118
|
-
.sum();
|
|
3119
|
-
print!("\x1b[{}G", 4 + visual_cursor_pos);
|
|
3120
|
-
let _ = io::stdout().flush();
|
|
3121
|
-
enable_raw_mode()?;
|
|
3122
3610
|
}
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
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();
|
|
3132
3633
|
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
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
|
+
}
|
|
3137
3638
|
|
|
3138
|
-
|
|
3139
|
-
|
|
3639
|
+
println!(" {BLUE}You ›{RESET} {}", expanded_str);
|
|
3640
|
+
let _ = io::stdout().flush();
|
|
3140
3641
|
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
}
|
|
3152
|
-
_ => {}
|
|
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;
|
|
3153
3652
|
}
|
|
3653
|
+
_ => {}
|
|
3154
3654
|
}
|
|
3155
3655
|
}
|
|
3156
3656
|
}
|
|
@@ -3190,18 +3690,18 @@ fn format_placeholders(s: &str) -> String {
|
|
|
3190
3690
|
while i < chars.len() {
|
|
3191
3691
|
if chars[i] == '[' && i + 12 < chars.len() {
|
|
3192
3692
|
let slice: String = chars[i..i + 13].iter().collect();
|
|
3193
|
-
if slice == "[Pasted text #"
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
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;
|
|
3205
3705
|
}
|
|
3206
3706
|
}
|
|
3207
3707
|
result.push(chars[i]);
|
|
@@ -3232,6 +3732,22 @@ fn active_model<'a>(provider: &str, config: &'a mint_core::MintConfig) -> &'a st
|
|
|
3232
3732
|
"huggingface" => &config.hf_model,
|
|
3233
3733
|
"local_openai" => &config.local_model_name,
|
|
3234
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
|
+
}
|
|
3235
3751
|
_ => &config.gemini_model,
|
|
3236
3752
|
}
|
|
3237
3753
|
}
|
|
@@ -3566,6 +4082,7 @@ async fn run_github_overview(repo: &str, config: &MintConfig) -> Result<()> {
|
|
|
3566
4082
|
audio_data_uri: None,
|
|
3567
4083
|
document_attachment: None,
|
|
3568
4084
|
workspace_path: None,
|
|
4085
|
+
agent_id: None,
|
|
3569
4086
|
},
|
|
3570
4087
|
)
|
|
3571
4088
|
.await?;
|