@pheem49/mint 1.6.4 → 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 (34) hide show
  1. package/Cargo.toml +1 -1
  2. package/bin/mint +0 -0
  3. package/crates/mint-cli/src/agent.rs +42 -14
  4. package/crates/mint-cli/src/main.rs +389 -2
  5. package/crates/mint-cli/src/setup.rs +173 -32
  6. package/crates/mint-core/src/api_server.rs +39 -0
  7. package/crates/mint-core/src/config.rs +2 -2
  8. package/crates/mint-core/src/memory.rs +27 -1
  9. package/crates/mint-core/src/orchestration.rs +1 -1
  10. package/crates/mint-core/src/plugins.rs +32 -0
  11. package/crates/mint-core/tests/memory_persistence.rs +28 -0
  12. package/package.json +1 -1
  13. package/src/renderer/src/agentProgress.ts +39 -0
  14. package/src/renderer/src/components/ChatPanel.tsx +308 -34
  15. package/src/renderer/src/components/DashboardSidebar.tsx +3 -3
  16. package/src/renderer/src/components/MintDashboard.tsx +71 -27
  17. package/src/renderer/src/components/ModelPanel.tsx +2 -2
  18. package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
  19. package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
  20. package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
  21. package/src/renderer/src/css/styles.css +195 -17
  22. package/src/renderer/src/index.css +46 -0
  23. package/src/renderer/src/tauri.ts +25 -0
  24. package/src/renderer/src-web/App.tsx +16 -1
  25. package/src/renderer/src-web/agentProgress.ts +39 -0
  26. package/src/renderer/src-web/components/ChatPanel.tsx +309 -35
  27. package/src/renderer/src-web/components/DashboardSidebar.tsx +1 -1
  28. package/src/renderer/src-web/components/MintDashboard.tsx +53 -16
  29. package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
  30. package/src/renderer/src-web/css/styles.css +242 -18
  31. package/src/renderer/src-web/index.css +46 -0
  32. package/src/renderer/src-web/tauri.ts +25 -0
  33. package/src-tauri/src/lib.rs +17 -0
  34. package/src-tauri/tauri.conf.json +1 -1
package/Cargo.toml CHANGED
@@ -7,7 +7,7 @@ members = [
7
7
  resolver = "2"
8
8
 
9
9
  [workspace.package]
10
- version = "1.6.4"
10
+ version = "1.7.0"
11
11
  edition = "2024"
12
12
  license = "AGPL-3.0-only"
13
13
 
package/bin/mint CHANGED
Binary file
@@ -555,7 +555,7 @@ fn render_live_status(status: &mut LiveStatus) {
555
555
  let frames = &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
556
556
  let frame = frames[status.spinner_tick % frames.len()];
557
557
  status.spinner_tick += 1;
558
- lines.push(format!("{MINT}{frame}{RESET}{BRIGHT}{thinking}{RESET}"));
558
+ lines.push(format!("{MINT}{frame}{RESET} {BRIGHT}{thinking}{RESET}"));
559
559
  }
560
560
  if lines.is_empty() {
561
561
  return;
@@ -801,22 +801,50 @@ fn format_markdown_bold(text: &str) -> String {
801
801
  }
802
802
 
803
803
  if !in_code_block {
804
- let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
805
- if hash_count > 0 && hash_count <= 6 {
806
- let next_char = trimmed.chars().nth(hash_count);
807
- let is_heading = match next_char {
808
- Some(' ') => true,
809
- Some(c) => !c.is_alphanumeric(),
810
- None => true,
811
- };
812
- if is_heading {
813
- // Make the entire heading line bold and bright white
814
- formatted_line = format!("{}{}{}", BRIGHT, line, RESET);
804
+ let mut leading_spaces = 0;
805
+ let mut is_list_item = false;
806
+ let mut marker_char = '-';
807
+
808
+ for (idx, c) in line.char_indices() {
809
+ if c.is_whitespace() {
810
+ leading_spaces += c.len_utf8();
815
811
  } else {
816
- formatted_line = process_inline_bold(&formatted_line);
812
+ if c == '-' || c == '*' || c == '+' {
813
+ let after = &line[idx + c.len_utf8()..];
814
+ if after.chars().next().map_or(false, |c| c.is_whitespace()) {
815
+ is_list_item = true;
816
+ marker_char = c;
817
+ }
818
+ }
819
+ break;
817
820
  }
821
+ }
822
+
823
+ if is_list_item {
824
+ let marker_len = marker_char.len_utf8();
825
+ let mut new_line = String::new();
826
+ new_line.push_str(&line[..leading_spaces]);
827
+ new_line.push('•');
828
+ new_line.push_str(&line[leading_spaces + marker_len..]);
829
+ formatted_line = process_inline_bold(&new_line);
818
830
  } else {
819
- formatted_line = process_inline_bold(&formatted_line);
831
+ let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
832
+ if hash_count > 0 && hash_count <= 6 {
833
+ let next_char = trimmed.chars().nth(hash_count);
834
+ let is_heading = match next_char {
835
+ Some(' ') => true,
836
+ Some(c) => !c.is_alphanumeric(),
837
+ None => true,
838
+ };
839
+ if is_heading {
840
+ // Make the entire heading line bold and bright white
841
+ formatted_line = format!("{}{}{}", BRIGHT, line, RESET);
842
+ } else {
843
+ formatted_line = process_inline_bold(&formatted_line);
844
+ }
845
+ } else {
846
+ formatted_line = process_inline_bold(&formatted_line);
847
+ }
820
848
  }
821
849
  } else {
822
850
  // Do not format markdown inside code blocks
@@ -1084,6 +1084,284 @@ async fn launch_mint_target(target: String) -> Result<()> {
1084
1084
  Ok(())
1085
1085
  }
1086
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
+
1087
1365
  struct ActionStreamFilter {
1088
1366
  buffer: String,
1089
1367
  in_action: bool,
@@ -1293,6 +1571,7 @@ async fn handle_slash_command(
1293
1571
  ("/paste [prompt]", "Attach image from clipboard"),
1294
1572
  ("Ctrl+V", "Paste clipboard image as [Image #1]"),
1295
1573
  ("/learn <path>", "Import a persistent .md or .txt skill"),
1574
+ ("/plugins <name> [prompt]", "Generate a skill.md file for a plugin/skill"),
1296
1575
  ("/memory list", "Show recent interactions"),
1297
1576
  ("/memory clear", "Clear all interactions"),
1298
1577
  ("/memory get <key>", "Read a profile value"),
@@ -1676,6 +1955,101 @@ async fn handle_slash_command(
1676
1955
 
1677
1956
  "/exit" | "/quit" => Some(SlashResult::Exit),
1678
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
+
1679
2053
  "/code" => {
1680
2054
  if rest.is_empty() {
1681
2055
  println!("{WARN}/code requires a task description{RESET}\n");
@@ -1924,6 +2298,7 @@ async fn run_interactive_chat() -> Result<()> {
1924
2298
  let sent_image = image_uri.clone();
1925
2299
  let mut first_chunk = true;
1926
2300
  let mut filter = ActionStreamFilter::new();
2301
+ let mut formatter = CliStreamFormatter::new();
1927
2302
 
1928
2303
  let stream_result = orchestrate_chat_stream_with_fallback(
1929
2304
  &session.config,
@@ -1942,7 +2317,11 @@ async fn run_interactive_chat() -> Result<()> {
1942
2317
  print!("\r\x1b[2K{MINT}Mint:{RESET} ");
1943
2318
  }
1944
2319
  filter.process_chunk(&chunk, |text| {
1945
- print!("{}", text);
2320
+ for c in text.chars() {
2321
+ formatter.process_char(c, |formatted_c| {
2322
+ print!("{}", formatted_c);
2323
+ });
2324
+ }
1946
2325
  });
1947
2326
  let _ = io::stdout().flush();
1948
2327
  },
@@ -1950,7 +2329,14 @@ async fn run_interactive_chat() -> Result<()> {
1950
2329
  .await;
1951
2330
 
1952
2331
  let actions = filter.finalize(|text| {
1953
- 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);
1954
2340
  });
1955
2341
  let _ = io::stdout().flush();
1956
2342
 
@@ -2029,6 +2415,7 @@ const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
2029
2415
  ("/image", "Attach image from disk"),
2030
2416
  ("/paste", "Attach image from clipboard"),
2031
2417
  ("/learn", "Import persistent skill/instruction"),
2418
+ ("/plugins", "List or generate plugins/skills"),
2032
2419
  ("/memory", "Manage long-term memory store"),
2033
2420
  ("/mcp", "List configured MCP servers"),
2034
2421
  ("/stats", "Show session statistics"),