@pheem49/mint 1.6.3 → 1.7.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 +82 -21
- package/assets/Mint_CLI.png +0 -0
- package/assets/Mint_Desktop.png +0 -0
- package/assets/Mint_Web.png +0 -0
- package/assets/Mint_WorkSpace.png +0 -0
- package/assets/Setting.png +0 -0
- package/assets/bridges.svg +4 -0
- package/assets/chat.svg +3 -0
- package/assets/code.svg +4 -0
- package/assets/features.svg +3 -0
- package/assets/imagegen.svg +5 -0
- package/assets/live2d.svg +6 -0
- package/assets/memory.svg +5 -0
- package/assets/screencapture.svg +4 -0
- package/assets/setup.svg +3 -0
- package/assets/tools.svg +4 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +47 -15
- package/crates/mint-cli/src/main.rs +534 -7
- package/crates/mint-cli/src/onboard.rs +134 -1
- package/crates/mint-cli/src/setup.rs +173 -32
- package/crates/mint-core/src/api_server.rs +147 -3
- package/crates/mint-core/src/config.rs +22 -3
- package/crates/mint-core/src/image_gen.rs +895 -0
- package/crates/mint-core/src/lib.rs +4 -0
- package/crates/mint-core/src/memory.rs +27 -1
- package/crates/mint-core/src/orchestration.rs +1 -1
- package/crates/mint-core/src/plugins.rs +32 -0
- package/crates/mint-core/tests/memory_persistence.rs +28 -0
- package/package.json +1 -1
- package/src/bin/index.js +0 -0
- package/src/renderer/src/agentProgress.ts +39 -0
- package/src/renderer/src/components/ChatPanel.tsx +547 -47
- package/src/renderer/src/components/DashboardSidebar.tsx +12 -4
- package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src/components/MintDashboard.tsx +84 -31
- package/src/renderer/src/components/ModelPanel.tsx +2 -2
- package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src/components/SettingsWindow.tsx +29 -20
- package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
- package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
- package/src/renderer/src/css/image-studio.css +722 -0
- package/src/renderer/src/css/settings.css +13 -3
- package/src/renderer/src/css/styles.css +245 -43
- package/src/renderer/src/index.css +152 -0
- package/src/renderer/src/tauri.ts +148 -0
- package/src/renderer/src-web/App.tsx +16 -1
- package/src/renderer/src-web/agentProgress.ts +39 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +568 -46
- package/src/renderer/src-web/components/DashboardSidebar.tsx +10 -2
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +65 -19
- package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +29 -20
- package/src/renderer/src-web/css/image-studio.css +722 -0
- package/src/renderer/src-web/css/settings.css +14 -4
- package/src/renderer/src-web/css/styles.css +292 -44
- package/src/renderer/src-web/index.css +152 -0
- package/src/renderer/src-web/tauri.ts +149 -0
- package/src-tauri/src/lib.rs +65 -5
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -7,10 +7,10 @@ use std::{
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
use mint_core::{
|
|
10
|
-
CHAT_CLI_ID, Capability, ChatRequest, CodeEdit, CodePatchHunk,
|
|
11
|
-
MintConfig, TaskStore, apply_code_edits, assert_path_capability, build_code_patch,
|
|
10
|
+
CHAT_CLI_ID, Capability, ChatRequest, CodeEdit, CodePatchHunk, ImageGenRequest, KnowledgeStore,
|
|
11
|
+
MemoryStore, MintConfig, TaskStore, apply_code_edits, assert_path_capability, build_code_patch,
|
|
12
12
|
build_symbol_index, classify_shell_command, config_path, create_folder, execute_native_plugin,
|
|
13
|
-
fetch_github_repo_summary, find_paths, index_semantic_code, initialize_config,
|
|
13
|
+
fetch_github_repo_summary, find_paths, generate_images, index_semantic_code, initialize_config,
|
|
14
14
|
inspect_code_plan, list_code_files, load_config, native_plugins,
|
|
15
15
|
orchestrate_chat_stream_with_fallback, orchestrate_chat_with_fallback, parse_github_url,
|
|
16
16
|
propose_code_edits, read_code_file, repository_summary, run_shell_command, search_code,
|
|
@@ -180,6 +180,23 @@ enum Command {
|
|
|
180
180
|
Onboard,
|
|
181
181
|
/// Interactively manage enabled agent tools.
|
|
182
182
|
Setup,
|
|
183
|
+
/// Generate an image from a text prompt using NanoBanana (Gemini image model).
|
|
184
|
+
Imagine {
|
|
185
|
+
/// Text description of the image to generate.
|
|
186
|
+
prompt: String,
|
|
187
|
+
/// Aspect ratio: 1:1, 16:9, 9:16, or 4:3 [default: 1:1]
|
|
188
|
+
#[arg(long, default_value = "1:1")]
|
|
189
|
+
aspect: String,
|
|
190
|
+
/// Number of images to generate (1–4) [default: 1]
|
|
191
|
+
#[arg(long, default_value_t = 1)]
|
|
192
|
+
count: u8,
|
|
193
|
+
/// Negative prompt — elements to avoid in the image
|
|
194
|
+
#[arg(long)]
|
|
195
|
+
negative: Option<String>,
|
|
196
|
+
/// Save the first generated image to this path
|
|
197
|
+
#[arg(long)]
|
|
198
|
+
output: Option<PathBuf>,
|
|
199
|
+
},
|
|
183
200
|
}
|
|
184
201
|
|
|
185
202
|
#[derive(Debug, Subcommand)]
|
|
@@ -926,6 +943,72 @@ async fn main() -> Result<()> {
|
|
|
926
943
|
launch_mint_target(target).await?;
|
|
927
944
|
}
|
|
928
945
|
}
|
|
946
|
+
Command::Imagine {
|
|
947
|
+
prompt,
|
|
948
|
+
aspect,
|
|
949
|
+
count,
|
|
950
|
+
negative,
|
|
951
|
+
output,
|
|
952
|
+
} => {
|
|
953
|
+
let config = load_config()?;
|
|
954
|
+
let count = count.clamp(1, 4);
|
|
955
|
+
eprint!("{DIM}✦ Generating {count} image(s)...{RESET}");
|
|
956
|
+
let _ = std::io::stderr().flush();
|
|
957
|
+
let request = ImageGenRequest {
|
|
958
|
+
prompt: prompt.clone(),
|
|
959
|
+
negative_prompt: negative,
|
|
960
|
+
aspect_ratio: Some(aspect),
|
|
961
|
+
num_images: Some(count),
|
|
962
|
+
model: None,
|
|
963
|
+
provider: None,
|
|
964
|
+
};
|
|
965
|
+
match generate_images(&config, &request).await {
|
|
966
|
+
Ok(result) => {
|
|
967
|
+
eprintln!(
|
|
968
|
+
"\r{MINT}✦ Generated {} image(s) {RESET}",
|
|
969
|
+
result.images.len()
|
|
970
|
+
);
|
|
971
|
+
let data_uris: Vec<String> = result
|
|
972
|
+
.images
|
|
973
|
+
.iter()
|
|
974
|
+
.map(|img| img.data_uri.clone())
|
|
975
|
+
.collect();
|
|
976
|
+
match mint_core::save_chat_images(
|
|
977
|
+
data_uris,
|
|
978
|
+
Some(result.provider.clone()),
|
|
979
|
+
Some(prompt.clone()),
|
|
980
|
+
) {
|
|
981
|
+
Ok(saved) => {
|
|
982
|
+
for entry in &saved {
|
|
983
|
+
println!("{MINT}✓{RESET} Saved: {}", entry.path.display());
|
|
984
|
+
}
|
|
985
|
+
// If --output specified, copy first image there
|
|
986
|
+
if let (Some(out_path), Some(first)) = (&output, saved.first()) {
|
|
987
|
+
match std::fs::copy(&first.path, out_path) {
|
|
988
|
+
Ok(_) => println!(
|
|
989
|
+
"{MINT}✓{RESET} Copied to: {}",
|
|
990
|
+
out_path.display()
|
|
991
|
+
),
|
|
992
|
+
Err(e) => eprintln!(
|
|
993
|
+
"{WARN}Warning: could not copy to output path: {e}{RESET}"
|
|
994
|
+
),
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if let Some(desc) = &result.description {
|
|
998
|
+
if !desc.is_empty() {
|
|
999
|
+
println!("\n{DIM}{desc}{RESET}");
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
Err(e) => eprintln!("{ERROR}Failed to save images: {e}{RESET}"),
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
Err(e) => {
|
|
1007
|
+
eprintln!("{ERROR}✗ Image generation failed: {e}{RESET}");
|
|
1008
|
+
anyhow::bail!("image generation failed: {e}");
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
929
1012
|
},
|
|
930
1013
|
}
|
|
931
1014
|
Ok(())
|
|
@@ -1001,6 +1084,284 @@ async fn launch_mint_target(target: String) -> Result<()> {
|
|
|
1001
1084
|
Ok(())
|
|
1002
1085
|
}
|
|
1003
1086
|
|
|
1087
|
+
fn is_table_line(line: &str) -> bool {
|
|
1088
|
+
let trimmed = line.trim();
|
|
1089
|
+
trimmed.starts_with('|') && trimmed.ends_with('|') && trimmed.len() > 1
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
fn process_inline_bold(s: &str) -> String {
|
|
1093
|
+
let mut result = String::new();
|
|
1094
|
+
let parts = s.split("**");
|
|
1095
|
+
let mut is_bold = false;
|
|
1096
|
+
for part in parts {
|
|
1097
|
+
if is_bold {
|
|
1098
|
+
result.push_str("\x1b[1m");
|
|
1099
|
+
result.push_str(part);
|
|
1100
|
+
result.push_str("\x1b[0m");
|
|
1101
|
+
} else {
|
|
1102
|
+
result.push_str(part);
|
|
1103
|
+
}
|
|
1104
|
+
is_bold = !is_bold;
|
|
1105
|
+
}
|
|
1106
|
+
result
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
fn format_line(line: &str) -> String {
|
|
1110
|
+
let mut formatted = line.to_string();
|
|
1111
|
+
let trimmed = line.trim_start();
|
|
1112
|
+
if (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ")) && trimmed.len() >= 2 {
|
|
1113
|
+
let leading_len = line.len() - trimmed.len();
|
|
1114
|
+
let leading_spaces = &line[..leading_len];
|
|
1115
|
+
formatted = format!("{}•{}", leading_spaces, &trimmed[1..]);
|
|
1116
|
+
}
|
|
1117
|
+
process_inline_bold(&formatted)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
fn unicode_width(s: &str) -> usize {
|
|
1121
|
+
let mut count = 0;
|
|
1122
|
+
for c in s.chars() {
|
|
1123
|
+
let val = c as u32;
|
|
1124
|
+
if (0x0E31..=0x0E31).contains(&val)
|
|
1125
|
+
|| (0x0E34..=0x0E3A).contains(&val)
|
|
1126
|
+
|| (0x0E47..=0x0E4E).contains(&val)
|
|
1127
|
+
{
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
if is_cjk(c) {
|
|
1131
|
+
count += 2;
|
|
1132
|
+
} else {
|
|
1133
|
+
count += 1;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
count
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
fn is_cjk(c: char) -> bool {
|
|
1140
|
+
let val = c as u32;
|
|
1141
|
+
(0x4E00..=0x9FFF).contains(&val)
|
|
1142
|
+
|| (0x3400..=0x4DBF).contains(&val)
|
|
1143
|
+
|| (0x20000..=0x2A6DF).contains(&val)
|
|
1144
|
+
|| (0xAC00..=0xD7AF).contains(&val)
|
|
1145
|
+
|| (0x3040..=0x309F).contains(&val)
|
|
1146
|
+
|| (0x30A0..=0x30FF).contains(&val)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
fn render_markdown_table(table_lines: &[String]) -> String {
|
|
1150
|
+
let mut rows: Vec<Vec<String>> = Vec::new();
|
|
1151
|
+
for line in table_lines {
|
|
1152
|
+
let trimmed = line.trim();
|
|
1153
|
+
if trimmed.is_empty() {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
let content = &trimmed[1..trimmed.len() - 1];
|
|
1157
|
+
let content_escaped = content.replace("\\|", "\u{0000}");
|
|
1158
|
+
let cols: Vec<String> = content_escaped
|
|
1159
|
+
.split('|')
|
|
1160
|
+
.map(|s| s.replace("\u{0000}", "|").trim().to_string())
|
|
1161
|
+
.collect();
|
|
1162
|
+
rows.push(cols);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if rows.is_empty() {
|
|
1166
|
+
return String::new();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
let mut has_separator = false;
|
|
1170
|
+
let mut separator_idx = None;
|
|
1171
|
+
for (i, row) in rows.iter().enumerate() {
|
|
1172
|
+
if row.iter().all(|col| col.chars().all(|c| c == '-' || c == ':' || c == ' ' || c == '\t')) && !row.is_empty() {
|
|
1173
|
+
has_separator = true;
|
|
1174
|
+
separator_idx = Some(i);
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
let mut data_rows: Vec<Vec<String>> = Vec::new();
|
|
1180
|
+
let mut header_row: Option<Vec<String>> = None;
|
|
1181
|
+
|
|
1182
|
+
if has_separator {
|
|
1183
|
+
let sep_idx = separator_idx.unwrap();
|
|
1184
|
+
if sep_idx > 0 {
|
|
1185
|
+
header_row = Some(rows[sep_idx - 1].clone());
|
|
1186
|
+
for (i, row) in rows.iter().enumerate() {
|
|
1187
|
+
if i != sep_idx && i != sep_idx - 1 {
|
|
1188
|
+
data_rows.push(row.clone());
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
} else {
|
|
1192
|
+
for (i, row) in rows.iter().enumerate() {
|
|
1193
|
+
if i != sep_idx {
|
|
1194
|
+
data_rows.push(row.clone());
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
header_row = Some(rows[0].clone());
|
|
1200
|
+
data_rows = rows[1..].to_vec();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
1204
|
+
let mut col_widths = vec![0; num_cols];
|
|
1205
|
+
|
|
1206
|
+
if let Some(ref header) = header_row {
|
|
1207
|
+
for (i, col) in header.iter().enumerate() {
|
|
1208
|
+
if i < num_cols {
|
|
1209
|
+
col_widths[i] = col_widths[i].max(unicode_width(col));
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
for row in &data_rows {
|
|
1214
|
+
for (i, col) in row.iter().enumerate() {
|
|
1215
|
+
if i < num_cols {
|
|
1216
|
+
col_widths[i] = col_widths[i].max(unicode_width(col));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
let mut rendered = String::new();
|
|
1222
|
+
|
|
1223
|
+
let draw_border = |left: &str, mid: &str, right: &str, fill: &str, widths: &[usize]| -> String {
|
|
1224
|
+
let mut s = left.to_string();
|
|
1225
|
+
for (i, &w) in widths.iter().enumerate() {
|
|
1226
|
+
s.push_str(&fill.repeat(w + 2));
|
|
1227
|
+
if i < widths.len() - 1 {
|
|
1228
|
+
s.push_str(mid);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
s.push_str(right);
|
|
1232
|
+
s.push('\n');
|
|
1233
|
+
s
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
rendered.push_str(&draw_border("┌", "┬", "┐", "─", &col_widths));
|
|
1237
|
+
|
|
1238
|
+
if let Some(ref header) = header_row {
|
|
1239
|
+
rendered.push_str("│");
|
|
1240
|
+
for (i, col) in header.iter().enumerate() {
|
|
1241
|
+
let width = unicode_width(col);
|
|
1242
|
+
let padding = col_widths[i] - width;
|
|
1243
|
+
rendered.push_str(&format!(" \x1b[1;36m{}\x1b[0m{}", col, " ".repeat(padding + 1)));
|
|
1244
|
+
rendered.push_str("│");
|
|
1245
|
+
}
|
|
1246
|
+
rendered.push('\n');
|
|
1247
|
+
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
for (r_idx, row) in data_rows.iter().enumerate() {
|
|
1251
|
+
rendered.push_str("│");
|
|
1252
|
+
for i in 0..num_cols {
|
|
1253
|
+
let col_val = row.get(i).cloned().unwrap_or_default();
|
|
1254
|
+
let width = unicode_width(&col_val);
|
|
1255
|
+
let padding = col_widths[i] - width;
|
|
1256
|
+
rendered.push_str(&format!(" {}{}", col_val, " ".repeat(padding + 1)));
|
|
1257
|
+
rendered.push_str("│");
|
|
1258
|
+
}
|
|
1259
|
+
rendered.push('\n');
|
|
1260
|
+
|
|
1261
|
+
if r_idx < data_rows.len() - 1 {
|
|
1262
|
+
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
rendered.push_str(&draw_border("└", "┴", "┘", "─", &col_widths).trim_end());
|
|
1267
|
+
rendered
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
struct CliStreamFormatter {
|
|
1271
|
+
line_buffer: String,
|
|
1272
|
+
table_buffer: Vec<String>,
|
|
1273
|
+
in_code_block: bool,
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
impl CliStreamFormatter {
|
|
1277
|
+
fn new() -> Self {
|
|
1278
|
+
Self {
|
|
1279
|
+
line_buffer: String::new(),
|
|
1280
|
+
table_buffer: Vec::new(),
|
|
1281
|
+
in_code_block: false,
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
fn process_char(&mut self, c: char, mut print_fn: impl FnMut(char)) {
|
|
1286
|
+
self.line_buffer.push(c);
|
|
1287
|
+
if c == '\n' {
|
|
1288
|
+
let mut line = self.line_buffer.clone();
|
|
1289
|
+
self.line_buffer.clear();
|
|
1290
|
+
if line.ends_with('\n') {
|
|
1291
|
+
line.pop();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
let trimmed = line.trim();
|
|
1295
|
+
if trimmed.starts_with("```") {
|
|
1296
|
+
self.in_code_block = !self.in_code_block;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if self.in_code_block {
|
|
1300
|
+
if !self.table_buffer.is_empty() {
|
|
1301
|
+
self.flush_table(&mut print_fn);
|
|
1302
|
+
}
|
|
1303
|
+
for ch in line.chars() {
|
|
1304
|
+
print_fn(ch);
|
|
1305
|
+
}
|
|
1306
|
+
print_fn('\n');
|
|
1307
|
+
} else {
|
|
1308
|
+
if is_table_line(&line) {
|
|
1309
|
+
self.table_buffer.push(line);
|
|
1310
|
+
} else {
|
|
1311
|
+
if !self.table_buffer.is_empty() {
|
|
1312
|
+
self.flush_table(&mut print_fn);
|
|
1313
|
+
}
|
|
1314
|
+
let formatted = format_line(&line);
|
|
1315
|
+
for ch in formatted.chars() {
|
|
1316
|
+
print_fn(ch);
|
|
1317
|
+
}
|
|
1318
|
+
print_fn('\n');
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
fn finalize(&mut self, mut print_fn: impl FnMut(char)) {
|
|
1325
|
+
if !self.line_buffer.is_empty() {
|
|
1326
|
+
let mut line = self.line_buffer.clone();
|
|
1327
|
+
self.line_buffer.clear();
|
|
1328
|
+
if line.ends_with('\n') {
|
|
1329
|
+
line.pop();
|
|
1330
|
+
}
|
|
1331
|
+
if self.in_code_block {
|
|
1332
|
+
if !self.table_buffer.is_empty() {
|
|
1333
|
+
self.flush_table(&mut print_fn);
|
|
1334
|
+
}
|
|
1335
|
+
for ch in line.chars() {
|
|
1336
|
+
print_fn(ch);
|
|
1337
|
+
}
|
|
1338
|
+
} else if is_table_line(&line) {
|
|
1339
|
+
self.table_buffer.push(line);
|
|
1340
|
+
} else {
|
|
1341
|
+
if !self.table_buffer.is_empty() {
|
|
1342
|
+
self.flush_table(&mut print_fn);
|
|
1343
|
+
}
|
|
1344
|
+
let formatted = format_line(&line);
|
|
1345
|
+
for ch in formatted.chars() {
|
|
1346
|
+
print_fn(ch);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if !self.table_buffer.is_empty() {
|
|
1351
|
+
self.flush_table(&mut print_fn);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
fn flush_table<F: FnMut(char)>(&mut self, mut print_fn: F) {
|
|
1356
|
+
let rendered = render_markdown_table(&self.table_buffer);
|
|
1357
|
+
for ch in rendered.chars() {
|
|
1358
|
+
print_fn(ch);
|
|
1359
|
+
}
|
|
1360
|
+
print_fn('\n');
|
|
1361
|
+
self.table_buffer.clear();
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1004
1365
|
struct ActionStreamFilter {
|
|
1005
1366
|
buffer: String,
|
|
1006
1367
|
in_action: bool,
|
|
@@ -1210,10 +1571,15 @@ async fn handle_slash_command(
|
|
|
1210
1571
|
("/paste [prompt]", "Attach image from clipboard"),
|
|
1211
1572
|
("Ctrl+V", "Paste clipboard image as [Image #1]"),
|
|
1212
1573
|
("/learn <path>", "Import a persistent .md or .txt skill"),
|
|
1574
|
+
("/plugins <name> [prompt]", "Generate a skill.md file for a plugin/skill"),
|
|
1213
1575
|
("/memory list", "Show recent interactions"),
|
|
1214
1576
|
("/memory clear", "Clear all interactions"),
|
|
1215
1577
|
("/memory get <key>", "Read a profile value"),
|
|
1216
1578
|
("/memory set <key> <val>", "Store a profile value"),
|
|
1579
|
+
(
|
|
1580
|
+
"/image-provider [name]",
|
|
1581
|
+
"List image gen providers or switch default provider",
|
|
1582
|
+
),
|
|
1217
1583
|
("/mcp list", "List configured MCP servers"),
|
|
1218
1584
|
("/mcp allow <server> <tool>", "Allow an MCP tool"),
|
|
1219
1585
|
("/stats", "Show session statistics"),
|
|
@@ -1270,6 +1636,55 @@ async fn handle_slash_command(
|
|
|
1270
1636
|
Some(SlashResult::Handled)
|
|
1271
1637
|
}
|
|
1272
1638
|
|
|
1639
|
+
"/image-provider" => {
|
|
1640
|
+
let mut available = Vec::new();
|
|
1641
|
+
if !session.config.api_key.trim().is_empty() {
|
|
1642
|
+
available.push("nanobanana");
|
|
1643
|
+
}
|
|
1644
|
+
if !session.config.openai_api_key.trim().is_empty() {
|
|
1645
|
+
available.push("dalle");
|
|
1646
|
+
}
|
|
1647
|
+
if !session.config.stability_api_key.trim().is_empty() {
|
|
1648
|
+
available.push("stability");
|
|
1649
|
+
}
|
|
1650
|
+
if !session.config.ideogram_api_key.trim().is_empty() {
|
|
1651
|
+
available.push("ideogram");
|
|
1652
|
+
}
|
|
1653
|
+
if !session.config.replicate_api_key.trim().is_empty() {
|
|
1654
|
+
available.push("replicate");
|
|
1655
|
+
}
|
|
1656
|
+
if available.is_empty() {
|
|
1657
|
+
available.push("nanobanana");
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if rest.is_empty() {
|
|
1661
|
+
println!("\n{BLUE}Configured image generation providers:{RESET}");
|
|
1662
|
+
for p in &available {
|
|
1663
|
+
let active = if p == &session.config.image_gen_provider.as_str() {
|
|
1664
|
+
format!(" {MINT}← active{RESET}")
|
|
1665
|
+
} else {
|
|
1666
|
+
String::new()
|
|
1667
|
+
};
|
|
1668
|
+
println!(" {p}{active}");
|
|
1669
|
+
}
|
|
1670
|
+
println!();
|
|
1671
|
+
} else {
|
|
1672
|
+
if available.contains(&rest) {
|
|
1673
|
+
session.config.image_gen_provider = rest.to_owned();
|
|
1674
|
+
match mint_core::save_config(&session.config) {
|
|
1675
|
+
Ok(()) => println!(
|
|
1676
|
+
"{DIM}Switched default image provider to: {}{RESET}\n",
|
|
1677
|
+
session.config.image_gen_provider
|
|
1678
|
+
),
|
|
1679
|
+
Err(error) => println!("{ERROR}Config error:{RESET} {error}"),
|
|
1680
|
+
}
|
|
1681
|
+
} else {
|
|
1682
|
+
println!("{ERROR}Provider '{rest}' is not configured or invalid.{RESET}\n");
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
Some(SlashResult::Handled)
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1273
1688
|
"/clear" | "/reset" => {
|
|
1274
1689
|
println!("Clear conversation history? [y/N] ");
|
|
1275
1690
|
if let Ok(true) = confirm("Clear conversation history? [y/N] ") {
|
|
@@ -1540,6 +1955,101 @@ async fn handle_slash_command(
|
|
|
1540
1955
|
|
|
1541
1956
|
"/exit" | "/quit" => Some(SlashResult::Exit),
|
|
1542
1957
|
|
|
1958
|
+
"/plugins" | "/plugin" => {
|
|
1959
|
+
let (plugin_name, prompt) = rest
|
|
1960
|
+
.split_once(char::is_whitespace)
|
|
1961
|
+
.map(|(p, r)| (p, r.trim()))
|
|
1962
|
+
.unwrap_or((rest, ""));
|
|
1963
|
+
|
|
1964
|
+
if plugin_name.is_empty() {
|
|
1965
|
+
println!("\n{BLUE}────────────────────────────────────────────{RESET}");
|
|
1966
|
+
println!("{MINT} Available Plugins & Skills{RESET}");
|
|
1967
|
+
println!("{BLUE}────────────────────────────────────────────{RESET}");
|
|
1968
|
+
|
|
1969
|
+
println!("{MINT}Native Plugins:{RESET}");
|
|
1970
|
+
for p in native_plugins() {
|
|
1971
|
+
println!(" - {BLUE}{:<20}{RESET} {}", p.name, p.description);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
println!("\n{MINT}Custom Skills/Plugins (Active):{RESET}");
|
|
1975
|
+
let mut active_skills = std::collections::HashSet::new();
|
|
1976
|
+
if let Ok(memory) = MemoryStore::open_default() {
|
|
1977
|
+
match memory.learned_skills(100) {
|
|
1978
|
+
Ok(skills) => {
|
|
1979
|
+
if skills.is_empty() {
|
|
1980
|
+
println!(" {DIM}(No custom skills/plugins learned yet. Use '/learn <path>' to add one.){RESET}");
|
|
1981
|
+
} else {
|
|
1982
|
+
for s in &skills {
|
|
1983
|
+
println!(" - {BLUE}{:<20}{RESET} {}", s.name, s.source_path);
|
|
1984
|
+
|
|
1985
|
+
// Try to store canonicalized active path
|
|
1986
|
+
if let Ok(canon) = Path::new(&s.source_path).canonicalize() {
|
|
1987
|
+
active_skills.insert(canon);
|
|
1988
|
+
} else {
|
|
1989
|
+
active_skills.insert(PathBuf::from(&s.source_path));
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
Err(e) => println!(" {ERROR}Error loading custom skills: {e}{RESET}"),
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Scan local workspace skills directory
|
|
1999
|
+
let local_skills_dir = session.current_dir.join("skills");
|
|
2000
|
+
if local_skills_dir.is_dir() {
|
|
2001
|
+
if let Ok(entries) = fs::read_dir(&local_skills_dir) {
|
|
2002
|
+
let mut local_files = Vec::new();
|
|
2003
|
+
for entry in entries.flatten() {
|
|
2004
|
+
let path = entry.path();
|
|
2005
|
+
if path.is_file() {
|
|
2006
|
+
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
2007
|
+
if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
|
|
2008
|
+
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
|
2009
|
+
if let Ok(canon_path) = path.canonicalize() {
|
|
2010
|
+
local_files.push((name.to_owned(), canon_path));
|
|
2011
|
+
} else {
|
|
2012
|
+
local_files.push((name.to_owned(), path));
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
let inactive_files: Vec<_> = local_files
|
|
2021
|
+
.into_iter()
|
|
2022
|
+
.filter(|(_, full_path)| !active_skills.contains(full_path))
|
|
2023
|
+
.map(|(name, _)| name)
|
|
2024
|
+
.collect();
|
|
2025
|
+
|
|
2026
|
+
if !inactive_files.is_empty() {
|
|
2027
|
+
println!("\n{MINT}Local Skill Files (Found but not learned/active yet):{RESET}");
|
|
2028
|
+
for file in inactive_files {
|
|
2029
|
+
println!(" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}", file, file, file);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
println!();
|
|
2036
|
+
Some(SlashResult::Handled)
|
|
2037
|
+
} else {
|
|
2038
|
+
let agent_prompt = if prompt.is_empty() {
|
|
2039
|
+
format!(
|
|
2040
|
+
"Create a skill markdown file for the plugin '{}' at the path 'skills/{}.md' in the workspace. Write the appropriate instructions inside.",
|
|
2041
|
+
plugin_name, plugin_name
|
|
2042
|
+
)
|
|
2043
|
+
} else {
|
|
2044
|
+
format!(
|
|
2045
|
+
"Create a skill markdown file for the plugin '{}' at the path 'skills/{}.md' in the workspace based on these instructions: {}",
|
|
2046
|
+
plugin_name, plugin_name, prompt
|
|
2047
|
+
)
|
|
2048
|
+
};
|
|
2049
|
+
Some(SlashResult::ForwardToAgent(agent_prompt))
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1543
2053
|
"/code" => {
|
|
1544
2054
|
if rest.is_empty() {
|
|
1545
2055
|
println!("{WARN}/code requires a task description{RESET}\n");
|
|
@@ -1788,6 +2298,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
1788
2298
|
let sent_image = image_uri.clone();
|
|
1789
2299
|
let mut first_chunk = true;
|
|
1790
2300
|
let mut filter = ActionStreamFilter::new();
|
|
2301
|
+
let mut formatter = CliStreamFormatter::new();
|
|
1791
2302
|
|
|
1792
2303
|
let stream_result = orchestrate_chat_stream_with_fallback(
|
|
1793
2304
|
&session.config,
|
|
@@ -1806,7 +2317,11 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
1806
2317
|
print!("\r\x1b[2K{MINT}Mint:{RESET} ");
|
|
1807
2318
|
}
|
|
1808
2319
|
filter.process_chunk(&chunk, |text| {
|
|
1809
|
-
|
|
2320
|
+
for c in text.chars() {
|
|
2321
|
+
formatter.process_char(c, |formatted_c| {
|
|
2322
|
+
print!("{}", formatted_c);
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
1810
2325
|
});
|
|
1811
2326
|
let _ = io::stdout().flush();
|
|
1812
2327
|
},
|
|
@@ -1814,7 +2329,14 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
1814
2329
|
.await;
|
|
1815
2330
|
|
|
1816
2331
|
let actions = filter.finalize(|text| {
|
|
1817
|
-
|
|
2332
|
+
for c in text.chars() {
|
|
2333
|
+
formatter.process_char(c, |formatted_c| {
|
|
2334
|
+
print!("{}", formatted_c);
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
formatter.finalize(|formatted_c| {
|
|
2339
|
+
print!("{}", formatted_c);
|
|
1818
2340
|
});
|
|
1819
2341
|
let _ = io::stdout().flush();
|
|
1820
2342
|
|
|
@@ -1884,11 +2406,16 @@ const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
|
|
|
1884
2406
|
("/help", "Show help menu"),
|
|
1885
2407
|
("/fast", "Toggle fast mode (hide thinking traces)"),
|
|
1886
2408
|
("/models", "List AI providers or switch active provider"),
|
|
2409
|
+
(
|
|
2410
|
+
"/image-provider",
|
|
2411
|
+
"List image gen providers or switch default provider",
|
|
2412
|
+
),
|
|
1887
2413
|
("/clear", "Clear conversation history"),
|
|
1888
2414
|
("/cd", "Change active workspace directory"),
|
|
1889
2415
|
("/image", "Attach image from disk"),
|
|
1890
2416
|
("/paste", "Attach image from clipboard"),
|
|
1891
2417
|
("/learn", "Import persistent skill/instruction"),
|
|
2418
|
+
("/plugins", "List or generate plugins/skills"),
|
|
1892
2419
|
("/memory", "Manage long-term memory store"),
|
|
1893
2420
|
("/mcp", "List configured MCP servers"),
|
|
1894
2421
|
("/stats", "Show session statistics"),
|
|
@@ -1963,9 +2490,9 @@ fn draw_input_box(
|
|
|
1963
2490
|
let highlight_idx = tab_index.map(|idx| idx % matches.len());
|
|
1964
2491
|
for (i, (cmd, desc)) in matches.iter().enumerate() {
|
|
1965
2492
|
if Some(i) == highlight_idx {
|
|
1966
|
-
println!(" {BLUE}▶ {:<
|
|
2493
|
+
println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
|
|
1967
2494
|
} else {
|
|
1968
|
-
println!(" {DIM}{:<
|
|
2495
|
+
println!(" {DIM}{:<16} - {}{RESET}", cmd, desc);
|
|
1969
2496
|
}
|
|
1970
2497
|
}
|
|
1971
2498
|
}
|