@pheem49/mint 1.7.0 → 1.8.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 +9 -0
- package/crates/mint-cli/src/agent.rs +1 -1
- package/crates/mint-cli/src/main.rs +47 -24
- package/crates/mint-cli/src/setup.rs +1 -1
- package/crates/mint-core/src/api_server.rs +64 -10
- package/crates/mint-core/src/chat.rs +79 -0
- package/crates/mint-core/src/lib.rs +1 -1
- package/crates/mint-core/src/plugins.rs +4 -1
- package/crates/mint-core/src/workflows.rs +8 -0
- package/package.json +13 -2
- package/src/renderer/src/App.tsx +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +27 -14
- package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
- package/src/renderer/src/components/ImageStudioPanel.tsx +7 -3
- package/src/renderer/src/components/MintDashboard.tsx +199 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
- package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
- package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src/components/Settings/PluginsTab.tsx +452 -0
- package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src/components/SettingsWindow.tsx +175 -962
- package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
- package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
- package/src/renderer/src/css/image-studio.css +9 -4
- package/src/renderer/src/css/settings.css +75 -6
- package/src/renderer/src/css/styles.css +355 -38
- package/src/renderer/src/css/workflows.css +542 -0
- package/src/renderer/src/env.d.ts +2 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +97 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +27 -14
- package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +7 -3
- package/src/renderer/src-web/components/MintDashboard.tsx +189 -0
- package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
- package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
- package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +452 -0
- package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +187 -968
- package/src/renderer/src-web/css/image-studio.css +9 -4
- package/src/renderer/src-web/css/settings.css +75 -6
- package/src/renderer/src-web/css/styles.css +294 -38
- package/src/renderer/src-web/env.d.ts +2 -0
- package/src/renderer/src-web/tauri.ts +36 -0
- package/src-tauri/src/lib.rs +84 -92
- package/src-tauri/tauri.conf.json +1 -1
package/Cargo.lock
CHANGED
|
@@ -2095,7 +2095,7 @@ dependencies = [
|
|
|
2095
2095
|
|
|
2096
2096
|
[[package]]
|
|
2097
2097
|
name = "mint-cli"
|
|
2098
|
-
version = "1.
|
|
2098
|
+
version = "1.8.0"
|
|
2099
2099
|
dependencies = [
|
|
2100
2100
|
"anyhow",
|
|
2101
2101
|
"base64 0.22.1",
|
|
@@ -2113,7 +2113,7 @@ dependencies = [
|
|
|
2113
2113
|
|
|
2114
2114
|
[[package]]
|
|
2115
2115
|
name = "mint-core"
|
|
2116
|
-
version = "1.
|
|
2116
|
+
version = "1.8.0"
|
|
2117
2117
|
dependencies = [
|
|
2118
2118
|
"base64 0.22.1",
|
|
2119
2119
|
"chrono",
|
|
@@ -2134,7 +2134,7 @@ dependencies = [
|
|
|
2134
2134
|
|
|
2135
2135
|
[[package]]
|
|
2136
2136
|
name = "mint-desktop"
|
|
2137
|
-
version = "1.
|
|
2137
|
+
version = "1.8.0"
|
|
2138
2138
|
dependencies = [
|
|
2139
2139
|
"base64 0.22.1",
|
|
2140
2140
|
"dirs",
|
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -95,6 +95,15 @@ Mint is a local-first AI assistant running on your machine, capable of handling
|
|
|
95
95
|
- Telegram, Discord Gateway, Discord RPC, Slack Socket Mode, LINE, and WhatsApp
|
|
96
96
|
Cloud API integrations.
|
|
97
97
|
- Signed Tauri update checks with an explicit approval step before installation.
|
|
98
|
+
- Dynamic local Ollama model fetching in the Settings Window to query and display the actual models installed on your machine.
|
|
99
|
+
- Pill-styled clean horizontal system event dividers for provider and model change notifications in the chat panel.
|
|
100
|
+
|
|
101
|
+
## What's New in v1.8.0
|
|
102
|
+
|
|
103
|
+
- **Integrated Web Search Settings**: Easily toggle between **Brave Search API** and **Google Custom Search API** directly from the General settings tab.
|
|
104
|
+
- **Image Generation Providers**: Choose and configure credentials for Stability AI, Ideogram, Replicate, OpenAI DALL-E, and Google NanoBanana (Gemini Images) inside dedicated inline settings cards.
|
|
105
|
+
- **Inline Productivity Settings**: Set up Client IDs, secrets, and API keys for native Gmail, Google Calendar, and Notion integrations inline inside the General settings tab.
|
|
106
|
+
- **Revamped Plugins Layout**: Moved **Learned AI Skills** (equivalent to `mint learn` in CLI) to the top of the Plugins tab, followed by MCP Servers (External tools), and Built-in Plugins (Spotify and Discord RPC) at the bottom.
|
|
98
107
|
|
|
99
108
|
## <img src="assets/setup.svg" width="24" height="24" valign="middle" /> Prerequisites
|
|
100
109
|
|
|
@@ -804,7 +804,7 @@ fn format_markdown_bold(text: &str) -> String {
|
|
|
804
804
|
let mut leading_spaces = 0;
|
|
805
805
|
let mut is_list_item = false;
|
|
806
806
|
let mut marker_char = '-';
|
|
807
|
-
|
|
807
|
+
|
|
808
808
|
for (idx, c) in line.char_indices() {
|
|
809
809
|
if c.is_whitespace() {
|
|
810
810
|
leading_spaces += c.len_utf8();
|
|
@@ -1109,7 +1109,9 @@ fn process_inline_bold(s: &str) -> String {
|
|
|
1109
1109
|
fn format_line(line: &str) -> String {
|
|
1110
1110
|
let mut formatted = line.to_string();
|
|
1111
1111
|
let trimmed = line.trim_start();
|
|
1112
|
-
if (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
|
|
1112
|
+
if (trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ "))
|
|
1113
|
+
&& trimmed.len() >= 2
|
|
1114
|
+
{
|
|
1113
1115
|
let leading_len = line.len() - trimmed.len();
|
|
1114
1116
|
let leading_spaces = &line[..leading_len];
|
|
1115
1117
|
formatted = format!("{}•{}", leading_spaces, &trimmed[1..]);
|
|
@@ -1169,7 +1171,11 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1169
1171
|
let mut has_separator = false;
|
|
1170
1172
|
let mut separator_idx = None;
|
|
1171
1173
|
for (i, row) in rows.iter().enumerate() {
|
|
1172
|
-
if row.iter().all(|col|
|
|
1174
|
+
if row.iter().all(|col| {
|
|
1175
|
+
col.chars()
|
|
1176
|
+
.all(|c| c == '-' || c == ':' || c == ' ' || c == '\t')
|
|
1177
|
+
}) && !row.is_empty()
|
|
1178
|
+
{
|
|
1173
1179
|
has_separator = true;
|
|
1174
1180
|
separator_idx = Some(i);
|
|
1175
1181
|
break;
|
|
@@ -1202,7 +1208,7 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1202
1208
|
|
|
1203
1209
|
let num_cols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
1204
1210
|
let mut col_widths = vec![0; num_cols];
|
|
1205
|
-
|
|
1211
|
+
|
|
1206
1212
|
if let Some(ref header) = header_row {
|
|
1207
1213
|
for (i, col) in header.iter().enumerate() {
|
|
1208
1214
|
if i < num_cols {
|
|
@@ -1220,18 +1226,19 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1220
1226
|
|
|
1221
1227
|
let mut rendered = String::new();
|
|
1222
1228
|
|
|
1223
|
-
let draw_border =
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
+
}
|
|
1229
1237
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
};
|
|
1238
|
+
s.push_str(right);
|
|
1239
|
+
s.push('\n');
|
|
1240
|
+
s
|
|
1241
|
+
};
|
|
1235
1242
|
|
|
1236
1243
|
rendered.push_str(&draw_border("┌", "┬", "┐", "─", &col_widths));
|
|
1237
1244
|
|
|
@@ -1240,7 +1247,11 @@ fn render_markdown_table(table_lines: &[String]) -> String {
|
|
|
1240
1247
|
for (i, col) in header.iter().enumerate() {
|
|
1241
1248
|
let width = unicode_width(col);
|
|
1242
1249
|
let padding = col_widths[i] - width;
|
|
1243
|
-
rendered.push_str(&format!(
|
|
1250
|
+
rendered.push_str(&format!(
|
|
1251
|
+
" \x1b[1;36m{}\x1b[0m{}",
|
|
1252
|
+
col,
|
|
1253
|
+
" ".repeat(padding + 1)
|
|
1254
|
+
));
|
|
1244
1255
|
rendered.push_str("│");
|
|
1245
1256
|
}
|
|
1246
1257
|
rendered.push('\n');
|
|
@@ -1571,7 +1582,10 @@ async fn handle_slash_command(
|
|
|
1571
1582
|
("/paste [prompt]", "Attach image from clipboard"),
|
|
1572
1583
|
("Ctrl+V", "Paste clipboard image as [Image #1]"),
|
|
1573
1584
|
("/learn <path>", "Import a persistent .md or .txt skill"),
|
|
1574
|
-
(
|
|
1585
|
+
(
|
|
1586
|
+
"/plugins <name> [prompt]",
|
|
1587
|
+
"Generate a skill.md file for a plugin/skill",
|
|
1588
|
+
),
|
|
1575
1589
|
("/memory list", "Show recent interactions"),
|
|
1576
1590
|
("/memory clear", "Clear all interactions"),
|
|
1577
1591
|
("/memory get <key>", "Read a profile value"),
|
|
@@ -1965,23 +1979,25 @@ async fn handle_slash_command(
|
|
|
1965
1979
|
println!("\n{BLUE}────────────────────────────────────────────{RESET}");
|
|
1966
1980
|
println!("{MINT} Available Plugins & Skills{RESET}");
|
|
1967
1981
|
println!("{BLUE}────────────────────────────────────────────{RESET}");
|
|
1968
|
-
|
|
1982
|
+
|
|
1969
1983
|
println!("{MINT}Native Plugins:{RESET}");
|
|
1970
1984
|
for p in native_plugins() {
|
|
1971
1985
|
println!(" - {BLUE}{:<20}{RESET} {}", p.name, p.description);
|
|
1972
1986
|
}
|
|
1973
|
-
|
|
1987
|
+
|
|
1974
1988
|
println!("\n{MINT}Custom Skills/Plugins (Active):{RESET}");
|
|
1975
1989
|
let mut active_skills = std::collections::HashSet::new();
|
|
1976
1990
|
if let Ok(memory) = MemoryStore::open_default() {
|
|
1977
1991
|
match memory.learned_skills(100) {
|
|
1978
1992
|
Ok(skills) => {
|
|
1979
1993
|
if skills.is_empty() {
|
|
1980
|
-
println!(
|
|
1994
|
+
println!(
|
|
1995
|
+
" {DIM}(No custom skills/plugins learned yet. Use '/learn <path>' to add one.){RESET}"
|
|
1996
|
+
);
|
|
1981
1997
|
} else {
|
|
1982
1998
|
for s in &skills {
|
|
1983
1999
|
println!(" - {BLUE}{:<20}{RESET} {}", s.name, s.source_path);
|
|
1984
|
-
|
|
2000
|
+
|
|
1985
2001
|
// Try to store canonicalized active path
|
|
1986
2002
|
if let Ok(canon) = Path::new(&s.source_path).canonicalize() {
|
|
1987
2003
|
active_skills.insert(canon);
|
|
@@ -2005,7 +2021,9 @@ async fn handle_slash_command(
|
|
|
2005
2021
|
if path.is_file() {
|
|
2006
2022
|
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
2007
2023
|
if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
|
|
2008
|
-
if let Some(name) =
|
|
2024
|
+
if let Some(name) =
|
|
2025
|
+
path.file_name().and_then(|n| n.to_str())
|
|
2026
|
+
{
|
|
2009
2027
|
if let Ok(canon_path) = path.canonicalize() {
|
|
2010
2028
|
local_files.push((name.to_owned(), canon_path));
|
|
2011
2029
|
} else {
|
|
@@ -2016,7 +2034,7 @@ async fn handle_slash_command(
|
|
|
2016
2034
|
}
|
|
2017
2035
|
}
|
|
2018
2036
|
}
|
|
2019
|
-
|
|
2037
|
+
|
|
2020
2038
|
let inactive_files: Vec<_> = local_files
|
|
2021
2039
|
.into_iter()
|
|
2022
2040
|
.filter(|(_, full_path)| !active_skills.contains(full_path))
|
|
@@ -2024,9 +2042,14 @@ async fn handle_slash_command(
|
|
|
2024
2042
|
.collect();
|
|
2025
2043
|
|
|
2026
2044
|
if !inactive_files.is_empty() {
|
|
2027
|
-
println!(
|
|
2045
|
+
println!(
|
|
2046
|
+
"\n{MINT}Local Skill Files (Found but not learned/active yet):{RESET}"
|
|
2047
|
+
);
|
|
2028
2048
|
for file in inactive_files {
|
|
2029
|
-
println!(
|
|
2049
|
+
println!(
|
|
2050
|
+
" - {WARN}{:<20}{RESET} skills/{} {DIM}(Type '/learn skills/{}' to activate){RESET}",
|
|
2051
|
+
file, file, file
|
|
2052
|
+
);
|
|
2030
2053
|
}
|
|
2031
2054
|
}
|
|
2032
2055
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
2
|
use crossterm::event::{self, Event, KeyCode};
|
|
3
3
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
|
4
|
-
use mint_core::{load_config,
|
|
4
|
+
use mint_core::{load_config, native_plugins, save_config};
|
|
5
5
|
use std::io::{self, Write};
|
|
6
6
|
|
|
7
7
|
struct ToolOption {
|
|
@@ -272,6 +272,58 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
272
272
|
.await;
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
|
+
("POST", "/api/interactions") => {
|
|
276
|
+
#[derive(Deserialize)]
|
|
277
|
+
#[serde(rename_all = "camelCase")]
|
|
278
|
+
struct ApiSaveInteraction {
|
|
279
|
+
chat_id: String,
|
|
280
|
+
user_text: String,
|
|
281
|
+
provider: String,
|
|
282
|
+
model: String,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if let Ok(req) = serde_json::from_str::<ApiSaveInteraction>(body) {
|
|
286
|
+
if let Ok(memory) = MemoryStore::open_default() {
|
|
287
|
+
match memory.add_interaction_for_chat(
|
|
288
|
+
&req.chat_id,
|
|
289
|
+
&req.user_text,
|
|
290
|
+
"",
|
|
291
|
+
&req.provider,
|
|
292
|
+
&req.model,
|
|
293
|
+
) {
|
|
294
|
+
Ok(row_id) => {
|
|
295
|
+
let res_json = json!({ "success": true, "id": row_id });
|
|
296
|
+
send_json_response(socket, "200 OK", &res_json.to_string())
|
|
297
|
+
.await;
|
|
298
|
+
}
|
|
299
|
+
Err(error) => {
|
|
300
|
+
let err_json =
|
|
301
|
+
json!({ "success": false, "message": error.to_string() });
|
|
302
|
+
send_json_response(
|
|
303
|
+
socket,
|
|
304
|
+
"500 Internal Server Error",
|
|
305
|
+
&err_json.to_string(),
|
|
306
|
+
)
|
|
307
|
+
.await;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
send_json_response(
|
|
312
|
+
socket,
|
|
313
|
+
"500 Internal Server Error",
|
|
314
|
+
"{\"success\":false,\"message\":\"db error\"}",
|
|
315
|
+
)
|
|
316
|
+
.await;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
send_json_response(
|
|
320
|
+
socket,
|
|
321
|
+
"400 Bad Request",
|
|
322
|
+
"{\"success\":false,\"message\":\"invalid body\"}",
|
|
323
|
+
)
|
|
324
|
+
.await;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
275
327
|
("POST", "/api/interactions/agent-activity") => {
|
|
276
328
|
#[derive(Deserialize)]
|
|
277
329
|
#[serde(rename_all = "camelCase")]
|
|
@@ -291,12 +343,8 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
291
343
|
)
|
|
292
344
|
.is_ok()
|
|
293
345
|
{
|
|
294
|
-
send_json_response(
|
|
295
|
-
|
|
296
|
-
"200 OK",
|
|
297
|
-
"{\"status\":\"ok\"}",
|
|
298
|
-
)
|
|
299
|
-
.await;
|
|
346
|
+
send_json_response(socket, "200 OK", "{\"status\":\"ok\"}")
|
|
347
|
+
.await;
|
|
300
348
|
return;
|
|
301
349
|
}
|
|
302
350
|
}
|
|
@@ -418,19 +466,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
418
466
|
chat_id: Option<String>,
|
|
419
467
|
image_data_uri: Option<String>,
|
|
420
468
|
audio_data_uri: Option<String>,
|
|
469
|
+
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
421
470
|
}
|
|
422
471
|
|
|
423
472
|
if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
|
|
424
473
|
let config = load_config().unwrap_or_default();
|
|
425
|
-
let
|
|
474
|
+
let chat_req = ChatRequest {
|
|
426
475
|
message: req.message,
|
|
427
476
|
system_instruction: req.system_instruction.unwrap_or_default(),
|
|
428
477
|
chat_id: req.chat_id,
|
|
429
478
|
image_data_uri: req.image_data_uri,
|
|
430
479
|
audio_data_uri: req.audio_data_uri,
|
|
431
|
-
document_attachment:
|
|
480
|
+
document_attachment: req.document_attachment,
|
|
432
481
|
workspace_path: None,
|
|
433
482
|
};
|
|
483
|
+
let mut chat_req =
|
|
484
|
+
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
434
485
|
let sent_image = chat_req.image_data_uri.clone();
|
|
435
486
|
let sent_message = chat_req.message.clone();
|
|
436
487
|
|
|
@@ -499,19 +550,22 @@ pub async fn start_api_server(port: u16) -> Result<(), std::io::Error> {
|
|
|
499
550
|
chat_id: Option<String>,
|
|
500
551
|
image_data_uri: Option<String>,
|
|
501
552
|
audio_data_uri: Option<String>,
|
|
553
|
+
document_attachment: Option<crate::chat::DocumentAttachment>,
|
|
502
554
|
}
|
|
503
555
|
|
|
504
556
|
if let Ok(req) = serde_json::from_str::<ApiChatRequest>(body) {
|
|
505
557
|
let config = load_config().unwrap_or_default();
|
|
506
|
-
let
|
|
558
|
+
let chat_req = ChatRequest {
|
|
507
559
|
message: req.message,
|
|
508
560
|
system_instruction: req.system_instruction.unwrap_or_default(),
|
|
509
561
|
chat_id: req.chat_id,
|
|
510
562
|
image_data_uri: req.image_data_uri,
|
|
511
563
|
audio_data_uri: req.audio_data_uri,
|
|
512
|
-
document_attachment:
|
|
564
|
+
document_attachment: req.document_attachment,
|
|
513
565
|
workspace_path: None,
|
|
514
566
|
};
|
|
567
|
+
let mut chat_req =
|
|
568
|
+
chat_req.with_document_context(&config).unwrap_or(chat_req);
|
|
515
569
|
let sent_image = chat_req.image_data_uri.clone();
|
|
516
570
|
let sent_message = chat_req.message.clone();
|
|
517
571
|
|
|
@@ -108,6 +108,85 @@ pub struct DocumentAttachment {
|
|
|
108
108
|
pub data_uri: String,
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
const MAX_DOCUMENT_BYTES: usize = 10 * 1024 * 1024;
|
|
112
|
+
const MAX_DOCUMENT_CONTEXT_CHARS: usize = 400_000;
|
|
113
|
+
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
|
114
|
+
|
|
115
|
+
impl ChatRequest {
|
|
116
|
+
pub fn with_document_context(&self, config: &crate::MintConfig) -> Result<ChatRequest, String> {
|
|
117
|
+
let Some(document) = &self.document_attachment else {
|
|
118
|
+
return Ok(self.clone());
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let (mime_type, encoded) = document
|
|
122
|
+
.data_uri
|
|
123
|
+
.strip_prefix("data:")
|
|
124
|
+
.and_then(|payload| payload.split_once(";base64,"))
|
|
125
|
+
.ok_or_else(|| "invalid PDF attachment data URI".to_string())?;
|
|
126
|
+
|
|
127
|
+
if !mime_type.eq_ignore_ascii_case("application/pdf") {
|
|
128
|
+
return Err("only PDF document attachments are supported".into());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
|
132
|
+
let bytes = BASE64_STANDARD
|
|
133
|
+
.decode(encoded)
|
|
134
|
+
.map_err(|error| format!("invalid PDF attachment encoding: {error}"))?;
|
|
135
|
+
if bytes.len() > MAX_DOCUMENT_BYTES {
|
|
136
|
+
return Err("PDF attachment is too large (> 10 MiB)".into());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let directory = crate::config::config_path()
|
|
140
|
+
.map_err(|error| error.to_string())?
|
|
141
|
+
.with_file_name("Attachments");
|
|
142
|
+
std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
|
|
143
|
+
|
|
144
|
+
let safe_name: String = document
|
|
145
|
+
.filename
|
|
146
|
+
.chars()
|
|
147
|
+
.filter(|c| c.is_ascii_alphanumeric() || matches!(*c, '.' | '-' | '_'))
|
|
148
|
+
.take(80)
|
|
149
|
+
.collect();
|
|
150
|
+
let safe_name = if safe_name.is_empty() {
|
|
151
|
+
"document".into()
|
|
152
|
+
} else {
|
|
153
|
+
safe_name
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
let path = directory.join(format!(
|
|
157
|
+
"mint-pdf-{}-{}.pdf",
|
|
158
|
+
COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
|
|
159
|
+
safe_name
|
|
160
|
+
));
|
|
161
|
+
std::fs::write(&path, bytes).map_err(|error| error.to_string())?;
|
|
162
|
+
|
|
163
|
+
let extracted = crate::knowledge::extract_document_text(&path, config)
|
|
164
|
+
.map_err(|error| error.to_string());
|
|
165
|
+
let _ = std::fs::remove_file(&path);
|
|
166
|
+
let extracted = extracted?;
|
|
167
|
+
|
|
168
|
+
let mut context: String = extracted.chars().take(MAX_DOCUMENT_CONTEXT_CHARS).collect();
|
|
169
|
+
if extracted.chars().count() > MAX_DOCUMENT_CONTEXT_CHARS {
|
|
170
|
+
context.push_str("\n\n[PDF text truncated because it is long.]");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let filename = document.filename.trim();
|
|
174
|
+
let filename = if filename.is_empty() {
|
|
175
|
+
"attached.pdf"
|
|
176
|
+
} else {
|
|
177
|
+
filename
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
let mut enriched = self.clone();
|
|
181
|
+
enriched.document_attachment = None;
|
|
182
|
+
enriched.message = format!(
|
|
183
|
+
"{}\n\nAttached PDF: {filename}\nUse the extracted PDF text below when answering. If the user asks for a summary, summarize this document.\n\n--- Extracted PDF text ---\n{context}\n--- End extracted PDF text ---",
|
|
184
|
+
self.message
|
|
185
|
+
);
|
|
186
|
+
Ok(enriched)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
111
190
|
#[derive(Debug, Clone, Serialize)]
|
|
112
191
|
#[serde(rename_all = "camelCase")]
|
|
113
192
|
pub struct ChatResponse {
|
|
@@ -78,7 +78,7 @@ pub use symbols::{CodeSymbol, SymbolError, SymbolIndex, build_symbol_index};
|
|
|
78
78
|
pub use tasks::{Task, TaskError, TaskStore, tasks_path};
|
|
79
79
|
pub use tts::{TtsUrl, google_tts_urls};
|
|
80
80
|
pub use weather::{WeatherError, WeatherReport, weather};
|
|
81
|
-
pub use workflows::{WorkflowError, load_workflows, workflows_path};
|
|
81
|
+
pub use workflows::{WorkflowError, load_workflows, save_workflows, workflows_path};
|
|
82
82
|
pub mod api_server;
|
|
83
83
|
pub use api_server::start_api_server;
|
|
84
84
|
pub mod channels;
|
|
@@ -242,7 +242,10 @@ fn github(instruction: &str) -> Result<String, PluginError> {
|
|
|
242
242
|
if id.chars().all(|c| c.is_ascii_digit()) {
|
|
243
243
|
run("gh", &["issue", "view", id])
|
|
244
244
|
} else {
|
|
245
|
-
invalid(
|
|
245
|
+
invalid(
|
|
246
|
+
"github",
|
|
247
|
+
"view_issue/issue view requires a numeric issue ID",
|
|
248
|
+
)
|
|
246
249
|
}
|
|
247
250
|
}
|
|
248
251
|
["list_prs"] | ["pr", "list"] => run("gh", &["pr", "list", "--limit", "10"]),
|
|
@@ -79,3 +79,11 @@ fn save_default_workflows(path: &PathBuf) -> Result<(), WorkflowError> {
|
|
|
79
79
|
source,
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
pub fn save_workflows(workflows: &[Value]) -> Result<(), WorkflowError> {
|
|
84
|
+
let path = workflows_path()?;
|
|
85
|
+
let directory = path.parent().ok_or(WorkflowError::MissingDirectory)?;
|
|
86
|
+
fs::create_dir_all(directory).map_err(WorkflowError::CreateDirectory)?;
|
|
87
|
+
let raw = serde_json::to_string_pretty(workflows).map_err(WorkflowError::Serialize)?;
|
|
88
|
+
fs::write(&path, format!("{raw}\n")).map_err(|source| WorkflowError::Write { path, source })
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pheem49/mint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "A native Tauri desktop AI assistant with a Rust backend and React UI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev:web": "vite --config vite.config.web.ts",
|
|
@@ -27,7 +27,17 @@
|
|
|
27
27
|
"cli": "cargo run -p mint-cli --",
|
|
28
28
|
"postinstall": "cargo build -p mint-cli --release && mkdir -p bin && cp target/release/mint bin/mint"
|
|
29
29
|
},
|
|
30
|
-
"keywords": [
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mint",
|
|
32
|
+
"tauri",
|
|
33
|
+
"ai",
|
|
34
|
+
"assistant",
|
|
35
|
+
"desktop",
|
|
36
|
+
"cli",
|
|
37
|
+
"rust",
|
|
38
|
+
"react",
|
|
39
|
+
"agent"
|
|
40
|
+
],
|
|
31
41
|
"repository": {
|
|
32
42
|
"type": "git",
|
|
33
43
|
"url": "git+https://github.com/Pheem49/Mint.git"
|
|
@@ -48,6 +58,7 @@
|
|
|
48
58
|
"dependencies": {
|
|
49
59
|
"@tauri-apps/api": "^2.0.0",
|
|
50
60
|
"@vitejs/plugin-react": "^6.0.1",
|
|
61
|
+
"@xyflow/react": "^12.11.1",
|
|
51
62
|
"material-icon-theme": "5.34.0",
|
|
52
63
|
"pixi-live2d-display": "^0.4.0",
|
|
53
64
|
"pixi.js": "^6.5.10",
|
package/src/renderer/src/App.tsx
CHANGED
|
@@ -6,7 +6,6 @@ const WidgetWindow = lazy(() => import('./components/WidgetWindow'))
|
|
|
6
6
|
const ProactiveGlow = lazy(() => import('./components/ProactiveGlow'))
|
|
7
7
|
const ScreenPicker = lazy(() => import('./components/ScreenPicker'))
|
|
8
8
|
const MintDashboard = lazy(() => import('./components/MintDashboard'))
|
|
9
|
-
|
|
10
9
|
export default function App() {
|
|
11
10
|
const [hash, setHash] = useState(window.location.hash || '#/')
|
|
12
11
|
|
|
@@ -29,5 +28,6 @@ export default function App() {
|
|
|
29
28
|
if (route.startsWith('/proactive-glow')) content = <ProactiveGlow />
|
|
30
29
|
if (route.startsWith('/screen-picker')) content = <ScreenPicker />
|
|
31
30
|
|
|
31
|
+
|
|
32
32
|
return <Suspense fallback={null}>{content}</Suspense>
|
|
33
33
|
}
|
|
@@ -1702,21 +1702,33 @@ export default function ChatPanel({
|
|
|
1702
1702
|
return (
|
|
1703
1703
|
<section className={`conversation-panel ${isEmptyChat ? 'is-empty' : ''}`}>
|
|
1704
1704
|
<div className="chat-container">
|
|
1705
|
-
{interactions.map((interaction) =>
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
<
|
|
1705
|
+
{interactions.map((interaction) => {
|
|
1706
|
+
const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
|
|
1707
|
+
if (isSystemEvent) {
|
|
1708
|
+
return (
|
|
1709
|
+
<div key={interaction.id} className="system-event-divider">
|
|
1710
|
+
<div className="system-event-line" />
|
|
1711
|
+
<div className="system-event-pill">
|
|
1712
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
1713
|
+
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
|
1714
|
+
</svg>
|
|
1715
|
+
<span>{interaction.userText}</span>
|
|
1716
1716
|
</div>
|
|
1717
|
+
<div className="system-event-line" />
|
|
1717
1718
|
</div>
|
|
1718
|
-
)
|
|
1719
|
-
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
return (
|
|
1722
|
+
<div key={interaction.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|
|
1723
|
+
{interaction.userText && (
|
|
1724
|
+
<div className="message user-message">
|
|
1725
|
+
<div className="bubble-wrapper">
|
|
1726
|
+
<div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
|
|
1727
|
+
<div className="message-time"><span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>
|
|
1728
|
+
</div>
|
|
1729
|
+
</div>
|
|
1730
|
+
)}
|
|
1731
|
+
<div className="message ai-message">
|
|
1720
1732
|
<div className="bubble-wrapper">
|
|
1721
1733
|
{renderCompletedActivity(interaction)}
|
|
1722
1734
|
{renderFileChanges(interaction)}
|
|
@@ -1750,7 +1762,8 @@ export default function ChatPanel({
|
|
|
1750
1762
|
</div>
|
|
1751
1763
|
</div>
|
|
1752
1764
|
</div>
|
|
1753
|
-
)
|
|
1765
|
+
);
|
|
1766
|
+
})}
|
|
1754
1767
|
|
|
1755
1768
|
{sending && (
|
|
1756
1769
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
|