@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.
Files changed (63) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +82 -21
  4. package/assets/Mint_CLI.png +0 -0
  5. package/assets/Mint_Desktop.png +0 -0
  6. package/assets/Mint_Web.png +0 -0
  7. package/assets/Mint_WorkSpace.png +0 -0
  8. package/assets/Setting.png +0 -0
  9. package/assets/bridges.svg +4 -0
  10. package/assets/chat.svg +3 -0
  11. package/assets/code.svg +4 -0
  12. package/assets/features.svg +3 -0
  13. package/assets/imagegen.svg +5 -0
  14. package/assets/live2d.svg +6 -0
  15. package/assets/memory.svg +5 -0
  16. package/assets/screencapture.svg +4 -0
  17. package/assets/setup.svg +3 -0
  18. package/assets/tools.svg +4 -0
  19. package/bin/mint +0 -0
  20. package/crates/mint-cli/src/agent.rs +47 -15
  21. package/crates/mint-cli/src/main.rs +534 -7
  22. package/crates/mint-cli/src/onboard.rs +134 -1
  23. package/crates/mint-cli/src/setup.rs +173 -32
  24. package/crates/mint-core/src/api_server.rs +147 -3
  25. package/crates/mint-core/src/config.rs +22 -3
  26. package/crates/mint-core/src/image_gen.rs +895 -0
  27. package/crates/mint-core/src/lib.rs +4 -0
  28. package/crates/mint-core/src/memory.rs +27 -1
  29. package/crates/mint-core/src/orchestration.rs +1 -1
  30. package/crates/mint-core/src/plugins.rs +32 -0
  31. package/crates/mint-core/tests/memory_persistence.rs +28 -0
  32. package/package.json +1 -1
  33. package/src/bin/index.js +0 -0
  34. package/src/renderer/src/agentProgress.ts +39 -0
  35. package/src/renderer/src/components/ChatPanel.tsx +547 -47
  36. package/src/renderer/src/components/DashboardSidebar.tsx +12 -4
  37. package/src/renderer/src/components/ImageStudioPanel.tsx +638 -0
  38. package/src/renderer/src/components/MintDashboard.tsx +84 -31
  39. package/src/renderer/src/components/ModelPanel.tsx +2 -2
  40. package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
  41. package/src/renderer/src/components/SettingsWindow.tsx +29 -20
  42. package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
  43. package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
  44. package/src/renderer/src/css/image-studio.css +722 -0
  45. package/src/renderer/src/css/settings.css +13 -3
  46. package/src/renderer/src/css/styles.css +245 -43
  47. package/src/renderer/src/index.css +152 -0
  48. package/src/renderer/src/tauri.ts +148 -0
  49. package/src/renderer/src-web/App.tsx +16 -1
  50. package/src/renderer/src-web/agentProgress.ts +39 -0
  51. package/src/renderer/src-web/components/ChatPanel.tsx +568 -46
  52. package/src/renderer/src-web/components/DashboardSidebar.tsx +10 -2
  53. package/src/renderer/src-web/components/ImageStudioPanel.tsx +638 -0
  54. package/src/renderer/src-web/components/MintDashboard.tsx +65 -19
  55. package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
  56. package/src/renderer/src-web/components/SettingsWindow.tsx +29 -20
  57. package/src/renderer/src-web/css/image-studio.css +722 -0
  58. package/src/renderer/src-web/css/settings.css +14 -4
  59. package/src/renderer/src-web/css/styles.css +292 -44
  60. package/src/renderer/src-web/index.css +152 -0
  61. package/src/renderer/src-web/tauri.ts +149 -0
  62. package/src-tauri/src/lib.rs +65 -5
  63. 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, KnowledgeStore, MemoryStore,
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
- print!("{}", text);
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
- print!("{}", text);
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}▶ {:<12}{RESET} {DIM}- {}{RESET}", cmd, desc);
2493
+ println!(" {BLUE}▶ {:<16}{RESET} {DIM}- {}{RESET}", cmd, desc);
1967
2494
  } else {
1968
- println!(" {DIM}{:<12} - {}{RESET}", cmd, desc);
2495
+ println!(" {DIM}{:<16} - {}{RESET}", cmd, desc);
1969
2496
  }
1970
2497
  }
1971
2498
  }