@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.
- package/Cargo.toml +1 -1
- package/bin/mint +0 -0
- package/crates/mint-cli/src/agent.rs +42 -14
- package/crates/mint-cli/src/main.rs +389 -2
- package/crates/mint-cli/src/setup.rs +173 -32
- package/crates/mint-core/src/api_server.rs +39 -0
- package/crates/mint-core/src/config.rs +2 -2
- 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/renderer/src/agentProgress.ts +39 -0
- package/src/renderer/src/components/ChatPanel.tsx +308 -34
- package/src/renderer/src/components/DashboardSidebar.tsx +3 -3
- package/src/renderer/src/components/MintDashboard.tsx +71 -27
- package/src/renderer/src/components/ModelPanel.tsx +2 -2
- package/src/renderer/src/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src/components/SpotlightWindow.tsx +8 -8
- package/src/renderer/src/components/WorkspacePanel.tsx +1 -1
- package/src/renderer/src/css/styles.css +195 -17
- package/src/renderer/src/index.css +46 -0
- package/src/renderer/src/tauri.ts +25 -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 +309 -35
- package/src/renderer/src-web/components/DashboardSidebar.tsx +1 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +53 -16
- package/src/renderer/src-web/components/PicturesLibrary.tsx +5 -1
- package/src/renderer/src-web/css/styles.css +242 -18
- package/src/renderer/src-web/index.css +46 -0
- package/src/renderer/src-web/tauri.ts +25 -0
- package/src-tauri/src/lib.rs +17 -0
- package/src-tauri/tauri.conf.json +1 -1
package/Cargo.toml
CHANGED
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
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"),
|