@pheem49/mint 1.7.1 → 1.8.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/.github/workflows/release.yml +1 -1
- package/Cargo.lock +34 -3
- package/Cargo.toml +2 -2
- package/README.md +40 -0
- package/Release_Note.md +95 -0
- package/assets/Mint_CLI.png +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/agent.rs +142 -48
- package/crates/mint-cli/src/main.rs +231 -22
- package/crates/mint-core/src/api_server.rs +71 -4
- package/crates/mint-core/src/browser.rs +401 -0
- package/crates/mint-core/src/chat.rs +79 -0
- package/crates/mint-core/src/config.rs +69 -1
- package/crates/mint-core/src/lib.rs +6 -1
- package/crates/mint-core/src/orchestration.rs +137 -1
- package/crates/mint-core/src/skills.rs +113 -4
- package/crates/mint-core/src/workflows.rs +9 -22
- package/package.json +2 -1
- package/src/renderer/src/App.tsx +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +197 -47
- package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
- package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
- package/src/renderer/src/components/MintDashboard.tsx +239 -7
- 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 +632 -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 -970
- package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
- package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +15 -4
- package/src/renderer/src/css/settings.css +75 -6
- package/src/renderer/src/css/styles.css +419 -49
- package/src/renderer/src/css/workflows.css +542 -0
- package/src/renderer/src/env.d.ts +2 -0
- package/src/renderer/src/index.css +13 -1
- package/src/renderer/src/tauri.ts +140 -2
- package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
- package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
- 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 +632 -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 -976
- package/src/renderer/src-web/css/image-studio.css +15 -4
- package/src/renderer/src-web/css/settings.css +75 -6
- package/src/renderer/src-web/css/styles.css +394 -50
- package/src/renderer/src-web/env.d.ts +2 -0
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +60 -2
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +195 -111
- package/src-tauri/src/workflows.rs +56 -4
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -134,6 +134,28 @@ export async function getRuntimeStatus(): Promise<RuntimeStatus> {
|
|
|
134
134
|
return invoke<RuntimeStatus>('get_runtime_status')
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
export interface DetectedTools {
|
|
138
|
+
docker: boolean
|
|
139
|
+
git: boolean
|
|
140
|
+
gh: boolean
|
|
141
|
+
node: boolean
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function detectSystemTools(): Promise<DetectedTools> {
|
|
145
|
+
if (typeof window === 'undefined' || !isTauriRuntime()) {
|
|
146
|
+
const API_BASE = getApiBase();
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch(`${API_BASE}/detect-tools`);
|
|
149
|
+
return await res.json();
|
|
150
|
+
} catch (e) {
|
|
151
|
+
console.error("Failed to detect tools from local server:", e);
|
|
152
|
+
return { docker: false, git: false, gh: false, node: false };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const { invoke } = await import('@tauri-apps/api/core')
|
|
156
|
+
return invoke<DetectedTools>('detect_system_tools')
|
|
157
|
+
}
|
|
158
|
+
|
|
137
159
|
export async function sendChatMessage(
|
|
138
160
|
message: string,
|
|
139
161
|
imageDataUri?: string | null,
|
|
@@ -293,6 +315,30 @@ export async function getRecentInteractions(limit = 50, chatId?: string | null):
|
|
|
293
315
|
return invoke<InteractionMemory[]>('get_recent_interactions', { limit, chatId })
|
|
294
316
|
}
|
|
295
317
|
|
|
318
|
+
export async function saveSystemInteraction(
|
|
319
|
+
chatId: string,
|
|
320
|
+
userText: string,
|
|
321
|
+
provider: string,
|
|
322
|
+
model: string,
|
|
323
|
+
): Promise<any> {
|
|
324
|
+
if (typeof window === 'undefined' || !isTauriRuntime()) {
|
|
325
|
+
const API_BASE = getApiBase();
|
|
326
|
+
try {
|
|
327
|
+
const res = await fetch(`${API_BASE}/interactions`, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify({ chatId, userText, provider, model }),
|
|
331
|
+
});
|
|
332
|
+
return await res.json();
|
|
333
|
+
} catch (e) {
|
|
334
|
+
console.error("Failed to save system interaction on local server:", e);
|
|
335
|
+
return { success: false };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const { invoke } = await import('@tauri-apps/api/core')
|
|
339
|
+
return invoke('save_system_interaction', { chatId, userText, provider, model })
|
|
340
|
+
}
|
|
341
|
+
|
|
296
342
|
export async function saveInteractionAgentActivity(
|
|
297
343
|
interactionId: number,
|
|
298
344
|
activity: AgentProgress[],
|
|
@@ -553,12 +599,12 @@ export async function setDefaultImageProvider(provider: string): Promise<boolean
|
|
|
553
599
|
|
|
554
600
|
|
|
555
601
|
|
|
556
|
-
export async function submitToolApproval(token: string, approved: boolean): Promise<void> {
|
|
602
|
+
export async function submitToolApproval(token: string, approved: boolean, answer?: string): Promise<void> {
|
|
557
603
|
if (typeof window === 'undefined' || !isTauriRuntime()) {
|
|
558
604
|
return;
|
|
559
605
|
}
|
|
560
606
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
561
|
-
return invoke('submit_tool_approval', { token, approved })
|
|
607
|
+
return invoke('submit_tool_approval', { token, approved, answer })
|
|
562
608
|
}
|
|
563
609
|
|
|
564
610
|
export async function proposeCodeEdits(root: string, edits: CodeEdit[]): Promise<CodeEditProposal> {
|
|
@@ -638,6 +684,7 @@ export function installTauriAdapters() {
|
|
|
638
684
|
openFolder: async () => ({ success: false, message: 'Opening local folders is only available in the desktop app.' }),
|
|
639
685
|
openCustomWorkflows: async () => ({ success: false, message: 'Opening workflow files is only available in the desktop app.' }),
|
|
640
686
|
reloadCustomWorkflows: async () => ({ success: false, message: 'Reloading workflow files is only available in the desktop app.' }),
|
|
687
|
+
saveCustomWorkflows: async () => ({ success: false, message: 'Saving workflow files is only available in the desktop app.' }),
|
|
641
688
|
};
|
|
642
689
|
|
|
643
690
|
(window as any).spotlightAPI = {
|
|
@@ -733,6 +780,9 @@ export function installTauriAdapters() {
|
|
|
733
780
|
openSettings: () => {
|
|
734
781
|
window.location.hash = '#/settings';
|
|
735
782
|
},
|
|
783
|
+
openWorkflows: () => {
|
|
784
|
+
window.location.hash = '#/workflows';
|
|
785
|
+
},
|
|
736
786
|
readClipboard: async () => '',
|
|
737
787
|
writeClipboard: async () => {},
|
|
738
788
|
getSystemInfo: async () => {
|
|
@@ -877,6 +927,10 @@ export function installTauriAdapters() {
|
|
|
877
927
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
878
928
|
return invoke('reload_custom_workflows')
|
|
879
929
|
},
|
|
930
|
+
saveCustomWorkflows: async (workflows) => {
|
|
931
|
+
const { invoke } = await import('@tauri-apps/api/core')
|
|
932
|
+
return invoke('save_custom_workflows', { workflows })
|
|
933
|
+
},
|
|
880
934
|
}
|
|
881
935
|
|
|
882
936
|
window.spotlightAPI = {
|
|
@@ -1003,6 +1057,10 @@ export function installTauriAdapters() {
|
|
|
1003
1057
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
1004
1058
|
return invoke('open_window', { kind: 'settings' })
|
|
1005
1059
|
},
|
|
1060
|
+
openWorkflows: async () => {
|
|
1061
|
+
const { invoke } = await import('@tauri-apps/api/core')
|
|
1062
|
+
return invoke('open_window', { kind: 'workflows' })
|
|
1063
|
+
},
|
|
1006
1064
|
readClipboard: () => navigator.clipboard.readText(),
|
|
1007
1065
|
writeClipboard: (text) => navigator.clipboard.writeText(text),
|
|
1008
1066
|
getSystemInfo: async () => ({ backend: 'rust' }),
|
package/src-tauri/src/browser.rs
CHANGED
|
@@ -1,141 +1 @@
|
|
|
1
|
-
|
|
2
|
-
use mint_core::MintConfig;
|
|
3
|
-
use serde::Serialize;
|
|
4
|
-
use serde_json::{Value, json};
|
|
5
|
-
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
|
6
|
-
|
|
7
|
-
#[derive(Debug, Clone, Serialize)]
|
|
8
|
-
#[serde(rename_all = "camelCase")]
|
|
9
|
-
pub struct BrowserTab {
|
|
10
|
-
pub id: String,
|
|
11
|
-
pub title: String,
|
|
12
|
-
pub url: String,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
pub async fn list_tabs(config: &MintConfig) -> Result<Vec<BrowserTab>, String> {
|
|
16
|
-
Ok(fetch_pages(config)
|
|
17
|
-
.await?
|
|
18
|
-
.into_iter()
|
|
19
|
-
.filter_map(|page| {
|
|
20
|
-
Some(BrowserTab {
|
|
21
|
-
id: page["id"].as_str()?.to_owned(),
|
|
22
|
-
title: page["title"].as_str().unwrap_or_default().to_owned(),
|
|
23
|
-
url: page["url"].as_str().unwrap_or_default().to_owned(),
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
.collect())
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
pub async fn navigate(config: &MintConfig, url: &str) -> Result<String, String> {
|
|
30
|
-
if !(url.starts_with("https://") || url.starts_with("http://")) {
|
|
31
|
-
return Err("browser navigation only supports http and https URLs".into());
|
|
32
|
-
}
|
|
33
|
-
let response = cdp_call(config, "Page.navigate", json!({ "url": url })).await?;
|
|
34
|
-
response["result"]["frameId"]
|
|
35
|
-
.as_str()
|
|
36
|
-
.map(|_| format!("navigating to {url}"))
|
|
37
|
-
.ok_or_else(|| response_error(&response))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
pub async fn read_page_text(config: &MintConfig) -> Result<String, String> {
|
|
41
|
-
let response = cdp_call(
|
|
42
|
-
config,
|
|
43
|
-
"Runtime.evaluate",
|
|
44
|
-
json!({
|
|
45
|
-
"expression": "document.body ? document.body.innerText.substring(0, 12000) : ''",
|
|
46
|
-
"returnByValue": true
|
|
47
|
-
}),
|
|
48
|
-
)
|
|
49
|
-
.await?;
|
|
50
|
-
response["result"]["result"]["value"]
|
|
51
|
-
.as_str()
|
|
52
|
-
.map(str::to_owned)
|
|
53
|
-
.ok_or_else(|| response_error(&response))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
pub async fn click(config: &MintConfig, selector: &str) -> Result<String, String> {
|
|
57
|
-
let selector = selector.trim();
|
|
58
|
-
if selector.is_empty() || selector.len() > 500 {
|
|
59
|
-
return Err("browser selector must contain between 1 and 500 characters".into());
|
|
60
|
-
}
|
|
61
|
-
let selector = serde_json::to_string(selector).map_err(|error| error.to_string())?;
|
|
62
|
-
let response = cdp_call(
|
|
63
|
-
config,
|
|
64
|
-
"Runtime.evaluate",
|
|
65
|
-
json!({
|
|
66
|
-
"expression": format!(
|
|
67
|
-
"(() => {{ const element = document.querySelector({selector}); if (!element) return 'not-found'; element.click(); return 'clicked'; }})()"
|
|
68
|
-
),
|
|
69
|
-
"returnByValue": true
|
|
70
|
-
}),
|
|
71
|
-
)
|
|
72
|
-
.await?;
|
|
73
|
-
match response["result"]["result"]["value"].as_str() {
|
|
74
|
-
Some("clicked") => Ok("clicked".into()),
|
|
75
|
-
Some("not-found") => Err(format!("browser selector not found: {selector}")),
|
|
76
|
-
_ => Err(response_error(&response)),
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Value, String> {
|
|
81
|
-
let page = fetch_pages(config)
|
|
82
|
-
.await?
|
|
83
|
-
.into_iter()
|
|
84
|
-
.find(|page| page["type"] == "page")
|
|
85
|
-
.ok_or("Chrome DevTools did not report an open browser page")?;
|
|
86
|
-
let socket_url = page["webSocketDebuggerUrl"]
|
|
87
|
-
.as_str()
|
|
88
|
-
.ok_or("Chrome DevTools page does not expose a websocket URL")?;
|
|
89
|
-
let (mut socket, _) = connect_async(socket_url)
|
|
90
|
-
.await
|
|
91
|
-
.map_err(|error| format!("unable to connect to Chrome DevTools websocket: {error}"))?;
|
|
92
|
-
socket
|
|
93
|
-
.send(Message::Text(
|
|
94
|
-
json!({ "id": 1, "method": method, "params": params })
|
|
95
|
-
.to_string()
|
|
96
|
-
.into(),
|
|
97
|
-
))
|
|
98
|
-
.await
|
|
99
|
-
.map_err(|error| error.to_string())?;
|
|
100
|
-
while let Some(message) = socket.next().await {
|
|
101
|
-
let message = message.map_err(|error| error.to_string())?;
|
|
102
|
-
let Message::Text(raw) = message else {
|
|
103
|
-
continue;
|
|
104
|
-
};
|
|
105
|
-
let value: Value = serde_json::from_str(&raw).map_err(|error| error.to_string())?;
|
|
106
|
-
if value["id"] == 1 {
|
|
107
|
-
return Ok(value);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
Err("Chrome DevTools websocket closed before returning a response".into())
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async fn fetch_pages(config: &MintConfig) -> Result<Vec<Value>, String> {
|
|
114
|
-
let endpoint = config
|
|
115
|
-
.extra
|
|
116
|
-
.get("browserDebugUrl")
|
|
117
|
-
.and_then(Value::as_str)
|
|
118
|
-
.unwrap_or("http://127.0.0.1:9222/json/list");
|
|
119
|
-
let value: Value = mint_core::HTTP_CLIENT
|
|
120
|
-
.clone()
|
|
121
|
-
.get(endpoint)
|
|
122
|
-
.send()
|
|
123
|
-
.await
|
|
124
|
-
.map_err(|error| format!("unable to reach Chrome DevTools at {endpoint}: {error}"))?
|
|
125
|
-
.error_for_status()
|
|
126
|
-
.map_err(|error| error.to_string())?
|
|
127
|
-
.json()
|
|
128
|
-
.await
|
|
129
|
-
.map_err(|error| error.to_string())?;
|
|
130
|
-
value
|
|
131
|
-
.as_array()
|
|
132
|
-
.cloned()
|
|
133
|
-
.ok_or_else(|| "Chrome DevTools response was not a page list".into())
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
fn response_error(response: &Value) -> String {
|
|
137
|
-
response["error"]["message"]
|
|
138
|
-
.as_str()
|
|
139
|
-
.unwrap_or("Chrome DevTools returned an unexpected response")
|
|
140
|
-
.to_owned()
|
|
141
|
-
}
|
|
1
|
+
// Deleted - Moved browser automation functions to mint-core.
|
|
@@ -8,11 +8,11 @@ use serde::Deserialize;
|
|
|
8
8
|
use serde_json::{Value, json};
|
|
9
9
|
use tauri::{AppHandle, Emitter, Manager};
|
|
10
10
|
|
|
11
|
-
use
|
|
11
|
+
use mint_core::browser::{click, navigate, read_page_text, type_text};
|
|
12
12
|
|
|
13
13
|
const MAX_STEPS: usize = 20;
|
|
14
14
|
const SYSTEM_PROMPT: &str = r#"You are Mint's native background task agent. Return only JSON:
|
|
15
|
-
{"thought":"short progress note","action":"done|propose_folder|propose_write_file|open_url|browser_read|browser_click|knowledge_search|propose_bash","target":"path, URL, selector, query, command, or final result","data":"optional file content"}
|
|
15
|
+
{"thought":"short progress note","action":"done|propose_folder|propose_write_file|open_url|browser_read|browser_click|browser_type|knowledge_search|propose_bash","target":"path, URL, selector, query, command, or final result","data":"optional file content"}
|
|
16
16
|
Use only one action per response. Background tasks never mutate the filesystem or execute shell commands. Use propose_folder, propose_write_file, and propose_bash so Mint can record a proposal for explicit user approval."#;
|
|
17
17
|
|
|
18
18
|
#[derive(Debug, Deserialize)]
|
|
@@ -177,6 +177,7 @@ async fn execute_action(
|
|
|
177
177
|
"open_url" => navigate(config, &action.target).await,
|
|
178
178
|
"browser_read" => read_page_text(config).await,
|
|
179
179
|
"browser_click" => click(config, &action.target).await,
|
|
180
|
+
"browser_type" => type_text(config, &action.target, &action.data).await,
|
|
180
181
|
"knowledge_search" => {
|
|
181
182
|
let hits = KnowledgeStore::open_default()
|
|
182
183
|
.map_err(|error| error.to_string())?
|