@pheem49/mint 1.6.4 → 1.7.1
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 +412 -2
- package/crates/mint-cli/src/setup.rs +173 -32
- package/crates/mint-core/src/api_server.rs +35 -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 +35 -0
- package/crates/mint-core/tests/memory_persistence.rs +28 -0
- package/package.json +12 -2
- 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 +16 -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,295 @@ 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("+ "))
|
|
1113
|
+
&& trimmed.len() >= 2
|
|
1114
|
+
{
|
|
1115
|
+
let leading_len = line.len() - trimmed.len();
|
|
1116
|
+
let leading_spaces = &line[..leading_len];
|
|
1117
|
+
formatted = format!("{}•{}", leading_spaces, &trimmed[1..]);
|
|
1118
|
+
}
|
|
1119
|
+
process_inline_bold(&formatted)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
fn unicode_width(s: &str) -> usize {
|
|
1123
|
+
let mut count = 0;
|
|
1124
|
+
for c in s.chars() {
|
|
1125
|
+
let val = c as u32;
|
|
1126
|
+
if (0x0E31..=0x0E31).contains(&val)
|
|
1127
|
+
|| (0x0E34..=0x0E3A).contains(&val)
|
|
1128
|
+
|| (0x0E47..=0x0E4E).contains(&val)
|
|
1129
|
+
{
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if is_cjk(c) {
|
|
1133
|
+
count += 2;
|
|
1134
|
+
} else {
|
|
1135
|
+
count += 1;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
count
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
fn is_cjk(c: char) -> bool {
|
|
1142
|
+
let val = c as u32;
|
|
1143
|
+
(0x4E00..=0x9FFF).contains(&val)
|
|
1144
|
+
|| (0x3400..=0x4DBF).contains(&val)
|
|
1145
|
+
|| (0x20000..=0x2A6DF).contains(&val)
|
|
1146
|
+
|| (0xAC00..=0xD7AF).contains(&val)
|
|
1147
|
+
|| (0x3040..=0x309F).contains(&val)
|
|
1148
|
+
|| (0x30A0..=0x30FF).contains(&val)
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
fn render_markdown_table(table_lines: &[String]) -> String {
|
|
1152
|
+
let mut rows: Vec<Vec<String>> = Vec::new();
|
|
1153
|
+
for line in table_lines {
|
|
1154
|
+
let trimmed = line.trim();
|
|
1155
|
+
if trimmed.is_empty() {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
let content = &trimmed[1..trimmed.len() - 1];
|
|
1159
|
+
let content_escaped = content.replace("\\|", "\u{0000}");
|
|
1160
|
+
let cols: Vec<String> = content_escaped
|
|
1161
|
+
.split('|')
|
|
1162
|
+
.map(|s| s.replace("\u{0000}", "|").trim().to_string())
|
|
1163
|
+
.collect();
|
|
1164
|
+
rows.push(cols);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if rows.is_empty() {
|
|
1168
|
+
return String::new();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
let mut has_separator = false;
|
|
1172
|
+
let mut separator_idx = None;
|
|
1173
|
+
for (i, row) in rows.iter().enumerate() {
|
|
1174
|
+
if row.iter().all(|col| {
|
|
1175
|
+
col.chars()
|
|
1176
|
+
.all(|c| c == '-' || c == ':' || c == ' ' || c == '\t')
|
|
1177
|
+
}) && !row.is_empty()
|
|
1178
|
+
{
|
|
1179
|
+
has_separator = true;
|
|
1180
|
+
separator_idx = Some(i);
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
let mut data_rows: Vec<Vec<String>> = Vec::new();
|
|
1186
|
+
let mut header_row: Option<Vec<String>> = None;
|
|
1187
|
+
|
|
1188
|
+
if has_separator {
|
|
1189
|
+
let sep_idx = separator_idx.unwrap();
|
|
1190
|
+
if sep_idx > 0 {
|
|
1191
|
+
header_row = Some(rows[sep_idx - 1].clone());
|
|
1192
|
+
for (i, row) in rows.iter().enumerate() {
|
|
1193
|
+
if i != sep_idx && i != sep_idx - 1 {
|
|
1194
|
+
data_rows.push(row.clone());
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
for (i, row) in rows.iter().enumerate() {
|
|
1199
|
+
if i != sep_idx {
|
|
1200
|
+
data_rows.push(row.clone());
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
} else {
|
|
1205
|
+
header_row = Some(rows[0].clone());
|
|
1206
|
+
data_rows = rows[1..].to_vec();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
1210
|
+
let mut col_widths = vec![0; num_cols];
|
|
1211
|
+
|
|
1212
|
+
if let Some(ref header) = header_row {
|
|
1213
|
+
for (i, col) in header.iter().enumerate() {
|
|
1214
|
+
if i < num_cols {
|
|
1215
|
+
col_widths[i] = col_widths[i].max(unicode_width(col));
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for row in &data_rows {
|
|
1220
|
+
for (i, col) in row.iter().enumerate() {
|
|
1221
|
+
if i < num_cols {
|
|
1222
|
+
col_widths[i] = col_widths[i].max(unicode_width(col));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
let mut rendered = String::new();
|
|
1228
|
+
|
|
1229
|
+
let draw_border =
|
|
1230
|
+
|left: &str, mid: &str, right: &str, fill: &str, widths: &[usize]| -> String {
|
|
1231
|
+
let mut s = left.to_string();
|
|
1232
|
+
for (i, &w) in widths.iter().enumerate() {
|
|
1233
|
+
s.push_str(&fill.repeat(w + 2));
|
|
1234
|
+
if i < widths.len() - 1 {
|
|
1235
|
+
s.push_str(mid);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
s.push_str(right);
|
|
1239
|
+
s.push('\n');
|
|
1240
|
+
s
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
rendered.push_str(&draw_border("┌", "┬", "┐", "─", &col_widths));
|
|
1244
|
+
|
|
1245
|
+
if let Some(ref header) = header_row {
|
|
1246
|
+
rendered.push_str("│");
|
|
1247
|
+
for (i, col) in header.iter().enumerate() {
|
|
1248
|
+
let width = unicode_width(col);
|
|
1249
|
+
let padding = col_widths[i] - width;
|
|
1250
|
+
rendered.push_str(&format!(
|
|
1251
|
+
" \x1b[1;36m{}\x1b[0m{}",
|
|
1252
|
+
col,
|
|
1253
|
+
" ".repeat(padding + 1)
|
|
1254
|
+
));
|
|
1255
|
+
rendered.push_str("│");
|
|
1256
|
+
}
|
|
1257
|
+
rendered.push('\n');
|
|
1258
|
+
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
for (r_idx, row) in data_rows.iter().enumerate() {
|
|
1262
|
+
rendered.push_str("│");
|
|
1263
|
+
for i in 0..num_cols {
|
|
1264
|
+
let col_val = row.get(i).cloned().unwrap_or_default();
|
|
1265
|
+
let width = unicode_width(&col_val);
|
|
1266
|
+
let padding = col_widths[i] - width;
|
|
1267
|
+
rendered.push_str(&format!(" {}{}", col_val, " ".repeat(padding + 1)));
|
|
1268
|
+
rendered.push_str("│");
|
|
1269
|
+
}
|
|
1270
|
+
rendered.push('\n');
|
|
1271
|
+
|
|
1272
|
+
if r_idx < data_rows.len() - 1 {
|
|
1273
|
+
rendered.push_str(&draw_border("├", "┼", "┤", "─", &col_widths));
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
rendered.push_str(&draw_border("└", "┴", "┘", "─", &col_widths).trim_end());
|
|
1278
|
+
rendered
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
struct CliStreamFormatter {
|
|
1282
|
+
line_buffer: String,
|
|
1283
|
+
table_buffer: Vec<String>,
|
|
1284
|
+
in_code_block: bool,
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
impl CliStreamFormatter {
|
|
1288
|
+
fn new() -> Self {
|
|
1289
|
+
Self {
|
|
1290
|
+
line_buffer: String::new(),
|
|
1291
|
+
table_buffer: Vec::new(),
|
|
1292
|
+
in_code_block: false,
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
fn process_char(&mut self, c: char, mut print_fn: impl FnMut(char)) {
|
|
1297
|
+
self.line_buffer.push(c);
|
|
1298
|
+
if c == '\n' {
|
|
1299
|
+
let mut line = self.line_buffer.clone();
|
|
1300
|
+
self.line_buffer.clear();
|
|
1301
|
+
if line.ends_with('\n') {
|
|
1302
|
+
line.pop();
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
let trimmed = line.trim();
|
|
1306
|
+
if trimmed.starts_with("```") {
|
|
1307
|
+
self.in_code_block = !self.in_code_block;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
if self.in_code_block {
|
|
1311
|
+
if !self.table_buffer.is_empty() {
|
|
1312
|
+
self.flush_table(&mut print_fn);
|
|
1313
|
+
}
|
|
1314
|
+
for ch in line.chars() {
|
|
1315
|
+
print_fn(ch);
|
|
1316
|
+
}
|
|
1317
|
+
print_fn('\n');
|
|
1318
|
+
} else {
|
|
1319
|
+
if is_table_line(&line) {
|
|
1320
|
+
self.table_buffer.push(line);
|
|
1321
|
+
} else {
|
|
1322
|
+
if !self.table_buffer.is_empty() {
|
|
1323
|
+
self.flush_table(&mut print_fn);
|
|
1324
|
+
}
|
|
1325
|
+
let formatted = format_line(&line);
|
|
1326
|
+
for ch in formatted.chars() {
|
|
1327
|
+
print_fn(ch);
|
|
1328
|
+
}
|
|
1329
|
+
print_fn('\n');
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
fn finalize(&mut self, mut print_fn: impl FnMut(char)) {
|
|
1336
|
+
if !self.line_buffer.is_empty() {
|
|
1337
|
+
let mut line = self.line_buffer.clone();
|
|
1338
|
+
self.line_buffer.clear();
|
|
1339
|
+
if line.ends_with('\n') {
|
|
1340
|
+
line.pop();
|
|
1341
|
+
}
|
|
1342
|
+
if self.in_code_block {
|
|
1343
|
+
if !self.table_buffer.is_empty() {
|
|
1344
|
+
self.flush_table(&mut print_fn);
|
|
1345
|
+
}
|
|
1346
|
+
for ch in line.chars() {
|
|
1347
|
+
print_fn(ch);
|
|
1348
|
+
}
|
|
1349
|
+
} else if is_table_line(&line) {
|
|
1350
|
+
self.table_buffer.push(line);
|
|
1351
|
+
} else {
|
|
1352
|
+
if !self.table_buffer.is_empty() {
|
|
1353
|
+
self.flush_table(&mut print_fn);
|
|
1354
|
+
}
|
|
1355
|
+
let formatted = format_line(&line);
|
|
1356
|
+
for ch in formatted.chars() {
|
|
1357
|
+
print_fn(ch);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if !self.table_buffer.is_empty() {
|
|
1362
|
+
self.flush_table(&mut print_fn);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
fn flush_table<F: FnMut(char)>(&mut self, mut print_fn: F) {
|
|
1367
|
+
let rendered = render_markdown_table(&self.table_buffer);
|
|
1368
|
+
for ch in rendered.chars() {
|
|
1369
|
+
print_fn(ch);
|
|
1370
|
+
}
|
|
1371
|
+
print_fn('\n');
|
|
1372
|
+
self.table_buffer.clear();
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1087
1376
|
struct ActionStreamFilter {
|
|
1088
1377
|
buffer: String,
|
|
1089
1378
|
in_action: bool,
|
|
@@ -1293,6 +1582,10 @@ async fn handle_slash_command(
|
|
|
1293
1582
|
("/paste [prompt]", "Attach image from clipboard"),
|
|
1294
1583
|
("Ctrl+V", "Paste clipboard image as [Image #1]"),
|
|
1295
1584
|
("/learn <path>", "Import a persistent .md or .txt skill"),
|
|
1585
|
+
(
|
|
1586
|
+
"/plugins <name> [prompt]",
|
|
1587
|
+
"Generate a skill.md file for a plugin/skill",
|
|
1588
|
+
),
|
|
1296
1589
|
("/memory list", "Show recent interactions"),
|
|
1297
1590
|
("/memory clear", "Clear all interactions"),
|
|
1298
1591
|
("/memory get <key>", "Read a profile value"),
|
|
@@ -1676,6 +1969,110 @@ async fn handle_slash_command(
|
|
|
1676
1969
|
|
|
1677
1970
|
"/exit" | "/quit" => Some(SlashResult::Exit),
|
|
1678
1971
|
|
|
1972
|
+
"/plugins" | "/plugin" => {
|
|
1973
|
+
let (plugin_name, prompt) = rest
|
|
1974
|
+
.split_once(char::is_whitespace)
|
|
1975
|
+
.map(|(p, r)| (p, r.trim()))
|
|
1976
|
+
.unwrap_or((rest, ""));
|
|
1977
|
+
|
|
1978
|
+
if plugin_name.is_empty() {
|
|
1979
|
+
println!("\n{BLUE}────────────────────────────────────────────{RESET}");
|
|
1980
|
+
println!("{MINT} Available Plugins & Skills{RESET}");
|
|
1981
|
+
println!("{BLUE}────────────────────────────────────────────{RESET}");
|
|
1982
|
+
|
|
1983
|
+
println!("{MINT}Native Plugins:{RESET}");
|
|
1984
|
+
for p in native_plugins() {
|
|
1985
|
+
println!(" - {BLUE}{:<20}{RESET} {}", p.name, p.description);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
println!("\n{MINT}Custom Skills/Plugins (Active):{RESET}");
|
|
1989
|
+
let mut active_skills = std::collections::HashSet::new();
|
|
1990
|
+
if let Ok(memory) = MemoryStore::open_default() {
|
|
1991
|
+
match memory.learned_skills(100) {
|
|
1992
|
+
Ok(skills) => {
|
|
1993
|
+
if skills.is_empty() {
|
|
1994
|
+
println!(
|
|
1995
|
+
" {DIM}(No custom skills/plugins learned yet. Use '/learn <path>' to add one.){RESET}"
|
|
1996
|
+
);
|
|
1997
|
+
} else {
|
|
1998
|
+
for s in &skills {
|
|
1999
|
+
println!(" - {BLUE}{:<20}{RESET} {}", s.name, s.source_path);
|
|
2000
|
+
|
|
2001
|
+
// Try to store canonicalized active path
|
|
2002
|
+
if let Ok(canon) = Path::new(&s.source_path).canonicalize() {
|
|
2003
|
+
active_skills.insert(canon);
|
|
2004
|
+
} else {
|
|
2005
|
+
active_skills.insert(PathBuf::from(&s.source_path));
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
Err(e) => println!(" {ERROR}Error loading custom skills: {e}{RESET}"),
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// Scan local workspace skills directory
|
|
2015
|
+
let local_skills_dir = session.current_dir.join("skills");
|
|
2016
|
+
if local_skills_dir.is_dir() {
|
|
2017
|
+
if let Ok(entries) = fs::read_dir(&local_skills_dir) {
|
|
2018
|
+
let mut local_files = Vec::new();
|
|
2019
|
+
for entry in entries.flatten() {
|
|
2020
|
+
let path = entry.path();
|
|
2021
|
+
if path.is_file() {
|
|
2022
|
+
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
2023
|
+
if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
|
|
2024
|
+
if let Some(name) =
|
|
2025
|
+
path.file_name().and_then(|n| n.to_str())
|
|
2026
|
+
{
|
|
2027
|
+
if let Ok(canon_path) = path.canonicalize() {
|
|
2028
|
+
local_files.push((name.to_owned(), canon_path));
|
|
2029
|
+
} else {
|
|
2030
|
+
local_files.push((name.to_owned(), path));
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
let inactive_files: Vec<_> = local_files
|
|
2039
|
+
.into_iter()
|
|
2040
|
+
.filter(|(_, full_path)| !active_skills.contains(full_path))
|
|
2041
|
+
.map(|(name, _)| name)
|
|
2042
|
+
.collect();
|
|
2043
|
+
|
|
2044
|
+
if !inactive_files.is_empty() {
|
|
2045
|
+
println!(
|
|
2046
|
+
"\n{MINT}Local Skill Files (Found but not learned/active yet):{RESET}"
|
|
2047
|
+
);
|
|
2048
|
+
for file in inactive_files {
|
|
2049
|
+
println!(
|
|
2050
|
+
" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
|
|
2051
|
+
file, file, file
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
println!();
|
|
2059
|
+
Some(SlashResult::Handled)
|
|
2060
|
+
} else {
|
|
2061
|
+
let agent_prompt = if prompt.is_empty() {
|
|
2062
|
+
format!(
|
|
2063
|
+
"Create a skill markdown file for the plugin '{}' at the path 'skills/{}.md' in the workspace. Write the appropriate instructions inside.",
|
|
2064
|
+
plugin_name, plugin_name
|
|
2065
|
+
)
|
|
2066
|
+
} else {
|
|
2067
|
+
format!(
|
|
2068
|
+
"Create a skill markdown file for the plugin '{}' at the path 'skills/{}.md' in the workspace based on these instructions: {}",
|
|
2069
|
+
plugin_name, plugin_name, prompt
|
|
2070
|
+
)
|
|
2071
|
+
};
|
|
2072
|
+
Some(SlashResult::ForwardToAgent(agent_prompt))
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1679
2076
|
"/code" => {
|
|
1680
2077
|
if rest.is_empty() {
|
|
1681
2078
|
println!("{WARN}/code requires a task description{RESET}\n");
|
|
@@ -1924,6 +2321,7 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
1924
2321
|
let sent_image = image_uri.clone();
|
|
1925
2322
|
let mut first_chunk = true;
|
|
1926
2323
|
let mut filter = ActionStreamFilter::new();
|
|
2324
|
+
let mut formatter = CliStreamFormatter::new();
|
|
1927
2325
|
|
|
1928
2326
|
let stream_result = orchestrate_chat_stream_with_fallback(
|
|
1929
2327
|
&session.config,
|
|
@@ -1942,7 +2340,11 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
1942
2340
|
print!("\r\x1b[2K{MINT}Mint:{RESET} ");
|
|
1943
2341
|
}
|
|
1944
2342
|
filter.process_chunk(&chunk, |text| {
|
|
1945
|
-
|
|
2343
|
+
for c in text.chars() {
|
|
2344
|
+
formatter.process_char(c, |formatted_c| {
|
|
2345
|
+
print!("{}", formatted_c);
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
1946
2348
|
});
|
|
1947
2349
|
let _ = io::stdout().flush();
|
|
1948
2350
|
},
|
|
@@ -1950,7 +2352,14 @@ async fn run_interactive_chat() -> Result<()> {
|
|
|
1950
2352
|
.await;
|
|
1951
2353
|
|
|
1952
2354
|
let actions = filter.finalize(|text| {
|
|
1953
|
-
|
|
2355
|
+
for c in text.chars() {
|
|
2356
|
+
formatter.process_char(c, |formatted_c| {
|
|
2357
|
+
print!("{}", formatted_c);
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
formatter.finalize(|formatted_c| {
|
|
2362
|
+
print!("{}", formatted_c);
|
|
1954
2363
|
});
|
|
1955
2364
|
let _ = io::stdout().flush();
|
|
1956
2365
|
|
|
@@ -2029,6 +2438,7 @@ const AUTOCOMPLETE_COMMANDS: &[(&str, &str)] = &[
|
|
|
2029
2438
|
("/image", "Attach image from disk"),
|
|
2030
2439
|
("/paste", "Attach image from clipboard"),
|
|
2031
2440
|
("/learn", "Import persistent skill/instruction"),
|
|
2441
|
+
("/plugins", "List or generate plugins/skills"),
|
|
2032
2442
|
("/memory", "Manage long-term memory store"),
|
|
2033
2443
|
("/mcp", "List configured MCP servers"),
|
|
2034
2444
|
("/stats", "Show session statistics"),
|