@pheem49/mint 1.8.0 → 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 +31 -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 +9 -0
- package/crates/mint-core/src/browser.rs +401 -0
- package/crates/mint-core/src/config.rs +69 -1
- package/crates/mint-core/src/lib.rs +5 -0
- 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 +1 -22
- package/package.json +1 -1
- package/src/renderer/src/components/ChatPanel.tsx +170 -33
- package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src/components/MintDashboard.tsx +40 -2
- package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src/components/SettingsWindow.tsx +1 -9
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +6 -0
- package/src/renderer/src/css/styles.css +64 -11
- package/src/renderer/src/index.css +12 -1
- package/src/renderer/src/tauri.ts +45 -4
- package/src/renderer/src-web/components/ChatPanel.tsx +163 -32
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +8 -3
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src-web/components/SettingsWindow.tsx +1 -9
- package/src/renderer/src-web/css/image-studio.css +6 -0
- package/src/renderer/src-web/css/styles.css +100 -12
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +24 -2
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +117 -26
- package/src-tauri/src/workflows.rs +56 -4
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -53,10 +53,32 @@ pub fn learn_skill(path: &Path) -> Result<LearnedSkill, SkillError> {
|
|
|
53
53
|
Ok(MemoryStore::open_default()?.add_learned_skill(name, &path.to_string_lossy(), &content)?)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
pub fn learned_skills_context() -> Result<String, SkillError> {
|
|
57
|
-
let skills = MemoryStore::open_default()?.learned_skills(20)?;
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
pub fn learned_skills_context(workspace_root: Option<&Path>) -> Result<String, SkillError> {
|
|
57
|
+
let mut skills = MemoryStore::open_default()?.learned_skills(20)?;
|
|
58
|
+
|
|
59
|
+
if let Some(home) = dirs::home_dir() {
|
|
60
|
+
let global_skills_path = home.join(".config").join("mint").join("mint-skills");
|
|
61
|
+
if !global_skills_path.exists() {
|
|
62
|
+
let _ = std::fs::create_dir_all(&global_skills_path);
|
|
63
|
+
}
|
|
64
|
+
load_skills_from_dir(&global_skills_path, &mut skills);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if let Some(root) = workspace_root {
|
|
68
|
+
let workspace_skills_path1 = root.join(".agents").join("skills");
|
|
69
|
+
load_skills_from_dir(&workspace_skills_path1, &mut skills);
|
|
70
|
+
|
|
71
|
+
let workspace_skills_path2 = root.join("skills");
|
|
72
|
+
load_skills_from_dir(&workspace_skills_path2, &mut skills);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let mut unique_skills = std::collections::BTreeMap::new();
|
|
76
|
+
for skill in skills {
|
|
77
|
+
unique_skills.insert(skill.name.clone(), skill);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let value = unique_skills
|
|
81
|
+
.into_values()
|
|
60
82
|
.map(|skill| format!("Skill: {}\n{}", skill.name, skill.content))
|
|
61
83
|
.collect::<Vec<_>>()
|
|
62
84
|
.join("\n\n");
|
|
@@ -69,3 +91,90 @@ pub fn learned_skills_context() -> Result<String, SkillError> {
|
|
|
69
91
|
}
|
|
70
92
|
Ok(format!("{}\n...<learned skills truncated>", &value[..end]))
|
|
71
93
|
}
|
|
94
|
+
|
|
95
|
+
pub fn load_skills_from_dir(dir: &Path, list: &mut Vec<LearnedSkill>) {
|
|
96
|
+
if !dir.is_dir() {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if let Ok(entries) = fs::read_dir(dir) {
|
|
100
|
+
for entry in entries.flatten() {
|
|
101
|
+
let path = entry.path();
|
|
102
|
+
if path.is_dir() {
|
|
103
|
+
for filename in &["SKILL.md", "SKILL.txt", "skill.md", "skill.txt"] {
|
|
104
|
+
let skill_file = path.join(filename);
|
|
105
|
+
if skill_file.is_file() {
|
|
106
|
+
if let Ok(content) = fs::read_to_string(&skill_file) {
|
|
107
|
+
let name = path
|
|
108
|
+
.file_name()
|
|
109
|
+
.and_then(|n| n.to_str())
|
|
110
|
+
.unwrap_or("skill")
|
|
111
|
+
.to_string();
|
|
112
|
+
list.push(LearnedSkill {
|
|
113
|
+
id: 0,
|
|
114
|
+
name,
|
|
115
|
+
source_path: skill_file.to_string_lossy().to_string(),
|
|
116
|
+
content,
|
|
117
|
+
created_at: String::new(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else if path.is_file() {
|
|
124
|
+
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
125
|
+
if matches!(ext.to_ascii_lowercase().as_str(), "md" | "txt") {
|
|
126
|
+
if let Ok(content) = fs::read_to_string(&path) {
|
|
127
|
+
let name = path
|
|
128
|
+
.file_stem()
|
|
129
|
+
.and_then(|n| n.to_str())
|
|
130
|
+
.unwrap_or("skill")
|
|
131
|
+
.to_string();
|
|
132
|
+
list.push(LearnedSkill {
|
|
133
|
+
id: 0,
|
|
134
|
+
name,
|
|
135
|
+
source_path: path.to_string_lossy().to_string(),
|
|
136
|
+
content,
|
|
137
|
+
created_at: String::new(),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[cfg(test)]
|
|
148
|
+
mod tests {
|
|
149
|
+
use super::*;
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn test_fs_skills_loading() {
|
|
153
|
+
let test_dir = std::env::temp_dir().join("mint_skills_test_unique_dir");
|
|
154
|
+
let _ = std::fs::remove_dir_all(&test_dir);
|
|
155
|
+
let workspace_path = test_dir.join(".agents").join("skills");
|
|
156
|
+
std::fs::create_dir_all(&workspace_path).unwrap();
|
|
157
|
+
|
|
158
|
+
// Write a file skill
|
|
159
|
+
let file_skill_path = workspace_path.join("rust-style.md");
|
|
160
|
+
std::fs::write(&file_skill_path, "Use 4 spaces for Rust indent.").unwrap();
|
|
161
|
+
|
|
162
|
+
// Write a directory skill
|
|
163
|
+
let dir_skill_path = workspace_path.join("js-style");
|
|
164
|
+
std::fs::create_dir_all(&dir_skill_path).unwrap();
|
|
165
|
+
std::fs::write(dir_skill_path.join("SKILL.md"), "Use 2 spaces for JS.").unwrap();
|
|
166
|
+
|
|
167
|
+
let mut list = Vec::new();
|
|
168
|
+
load_skills_from_dir(&workspace_path, &mut list);
|
|
169
|
+
|
|
170
|
+
let _ = std::fs::remove_dir_all(&test_dir);
|
|
171
|
+
|
|
172
|
+
assert_eq!(list.len(), 2);
|
|
173
|
+
|
|
174
|
+
let rust_skill = list.iter().find(|s| s.name == "rust-style").unwrap();
|
|
175
|
+
assert_eq!(rust_skill.content, "Use 4 spaces for Rust indent.");
|
|
176
|
+
|
|
177
|
+
let js_skill = list.iter().find(|s| s.name == "js-style").unwrap();
|
|
178
|
+
assert_eq!(js_skill.content, "Use 2 spaces for JS.");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -51,28 +51,7 @@ pub fn load_workflows() -> Result<Vec<Value>, WorkflowError> {
|
|
|
51
51
|
fn save_default_workflows(path: &PathBuf) -> Result<(), WorkflowError> {
|
|
52
52
|
let directory = path.parent().ok_or(WorkflowError::MissingDirectory)?;
|
|
53
53
|
fs::create_dir_all(directory).map_err(WorkflowError::CreateDirectory)?;
|
|
54
|
-
let workflows = json!([
|
|
55
|
-
{
|
|
56
|
-
"id": "wf-1",
|
|
57
|
-
"name": "Check Mic on Zoom",
|
|
58
|
-
"trigger": { "type": "process_running", "processName": "zoom" },
|
|
59
|
-
"action": {
|
|
60
|
-
"type": "system_info",
|
|
61
|
-
"message": "Looks like you opened Zoom. Should I check your system resources?",
|
|
62
|
-
"target": ""
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"id": "wf-2",
|
|
67
|
-
"name": "Coding Time",
|
|
68
|
-
"trigger": { "type": "process_running", "processName": "code" },
|
|
69
|
-
"action": {
|
|
70
|
-
"type": "open_app",
|
|
71
|
-
"message": "Coding time. Want me to open Spotify?",
|
|
72
|
-
"target": "spotify"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
]);
|
|
54
|
+
let workflows = json!([]);
|
|
76
55
|
let raw = serde_json::to_string_pretty(&workflows).map_err(WorkflowError::Serialize)?;
|
|
77
56
|
fs::write(path, format!("{raw}\n")).map_err(|source| WorkflowError::Write {
|
|
78
57
|
path: path.clone(),
|
package/package.json
CHANGED
|
@@ -68,15 +68,7 @@ const LOCAL_MODELS = [
|
|
|
68
68
|
'lmstudio-community/gemma-2-9b-it-GGUF'
|
|
69
69
|
]
|
|
70
70
|
|
|
71
|
-
const OLLAMA_MODELS = [
|
|
72
|
-
'llama3:latest',
|
|
73
|
-
'llama3.1:latest',
|
|
74
|
-
'llama3.2:latest',
|
|
75
|
-
'gemma2:latest',
|
|
76
|
-
'mistral:latest',
|
|
77
|
-
'phi3:latest',
|
|
78
|
-
'qwen2.5:latest'
|
|
79
|
-
]
|
|
71
|
+
const OLLAMA_MODELS: string[] = []
|
|
80
72
|
|
|
81
73
|
interface ApprovalDetails {
|
|
82
74
|
title: string
|
|
@@ -122,6 +114,7 @@ interface AgentActivity {
|
|
|
122
114
|
target: string
|
|
123
115
|
kind: 'file' | 'folder' | 'search' | 'terminal' | 'tool'
|
|
124
116
|
state: 'active' | 'done' | 'error'
|
|
117
|
+
action?: string
|
|
125
118
|
}
|
|
126
119
|
|
|
127
120
|
interface AgentActivityView {
|
|
@@ -153,7 +146,20 @@ function describeTool(action: string, input: Record<string, unknown>): AgentActi
|
|
|
153
146
|
const command = activityDetail(input, 'command')
|
|
154
147
|
const name = activityDetail(input, 'name')
|
|
155
148
|
const tool = activityDetail(input, 'tool')
|
|
156
|
-
const
|
|
149
|
+
const rawTarget = path || query || command || name || tool || action.replaceAll('_', ' ')
|
|
150
|
+
|
|
151
|
+
// Append line range for read_file when startLine / endLine are available
|
|
152
|
+
let target = rawTarget
|
|
153
|
+
if (action === 'read_file' && path) {
|
|
154
|
+
const startLine = typeof input.startLine === 'number' ? input.startLine : undefined
|
|
155
|
+
const endLine = typeof input.endLine === 'number' ? input.endLine : undefined
|
|
156
|
+
if (startLine !== undefined && endLine !== undefined) {
|
|
157
|
+
target = `${rawTarget} #L${startLine}-${endLine}`
|
|
158
|
+
} else if (startLine !== undefined) {
|
|
159
|
+
target = `${rawTarget} #L${startLine}`
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
157
163
|
const labels: Record<string, string> = {
|
|
158
164
|
apply_patch: 'Applying patch',
|
|
159
165
|
ask_user: 'Asking user',
|
|
@@ -189,6 +195,7 @@ function describeTool(action: string, input: Record<string, unknown>): AgentActi
|
|
|
189
195
|
target: formatActivityTarget(target),
|
|
190
196
|
kind: activityKind(action, target),
|
|
191
197
|
state: 'active',
|
|
198
|
+
action,
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
@@ -215,7 +222,13 @@ function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
|
|
|
215
222
|
for (let index = activities.length - 1; index >= 0; index -= 1) {
|
|
216
223
|
if (activities[index].state !== 'active') continue
|
|
217
224
|
activities[index].state = event.data.result.startsWith('Error:') ? 'error' : 'done'
|
|
218
|
-
if (activities[index].state === 'error')
|
|
225
|
+
if (activities[index].state === 'error') {
|
|
226
|
+
activities[index].label = 'Failed'
|
|
227
|
+
}
|
|
228
|
+
if (activities[index].action === 'ask_user' && event.data.result.startsWith('User answered:')) {
|
|
229
|
+
const answer = event.data.result.replace('User answered:', '').trim()
|
|
230
|
+
activities[index].target = `(Answered: "${answer}") ${activities[index].target}`
|
|
231
|
+
}
|
|
219
232
|
break
|
|
220
233
|
}
|
|
221
234
|
}
|
|
@@ -224,7 +237,16 @@ function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
|
|
|
224
237
|
return { summary: activitySummary(activities), items }
|
|
225
238
|
}
|
|
226
239
|
|
|
227
|
-
function
|
|
240
|
+
function AgentActivityTable({ activityView }: { activityView: AgentActivityView }) {
|
|
241
|
+
const [expandedIndices, setExpandedIndices] = useState<Record<number, boolean>>({})
|
|
242
|
+
|
|
243
|
+
const toggleExpand = (index: number) => {
|
|
244
|
+
setExpandedIndices(prev => ({
|
|
245
|
+
...prev,
|
|
246
|
+
[index]: !prev[index]
|
|
247
|
+
}))
|
|
248
|
+
}
|
|
249
|
+
|
|
228
250
|
return (
|
|
229
251
|
<div className="agent-activity-list">
|
|
230
252
|
<div className="agent-activity-table-head" aria-hidden="true">
|
|
@@ -233,14 +255,39 @@ function renderAgentActivityTable(activityView: AgentActivityView) {
|
|
|
233
255
|
<span>Target</span>
|
|
234
256
|
<span />
|
|
235
257
|
</div>
|
|
236
|
-
{activityView.items.map((activity, index) =>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
258
|
+
{activityView.items.map((activity, index) => {
|
|
259
|
+
const isExpanded = !!expandedIndices[index]
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
className="agent-activity-item"
|
|
263
|
+
data-kind={activity.kind}
|
|
264
|
+
data-state={activity.state}
|
|
265
|
+
key={`${index}-${activity.label}-${activity.target}`}
|
|
266
|
+
style={{ cursor: 'pointer' }}
|
|
267
|
+
onClick={() => toggleExpand(index)}
|
|
268
|
+
>
|
|
269
|
+
<span className="agent-activity-label">{activity.label}</span>
|
|
270
|
+
<span className="agent-activity-icon" aria-hidden="true" />
|
|
271
|
+
<span
|
|
272
|
+
className="agent-activity-text"
|
|
273
|
+
style={isExpanded ? { whiteSpace: 'normal', wordBreak: 'break-all', overflow: 'visible', textOverflow: 'clip' } : undefined}
|
|
274
|
+
>
|
|
275
|
+
{activity.target}
|
|
276
|
+
</span>
|
|
277
|
+
<span
|
|
278
|
+
className="agent-activity-chevron"
|
|
279
|
+
style={{
|
|
280
|
+
transform: isExpanded ? 'rotate(90deg)' : 'none',
|
|
281
|
+
transition: 'transform 0.2s ease',
|
|
282
|
+
display: 'inline-block'
|
|
283
|
+
}}
|
|
284
|
+
aria-hidden="true"
|
|
285
|
+
>
|
|
286
|
+
>
|
|
287
|
+
</span>
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
})}
|
|
244
291
|
</div>
|
|
245
292
|
)
|
|
246
293
|
}
|
|
@@ -257,7 +304,7 @@ function renderApprovalDetails(approval: any): ApprovalDetails {
|
|
|
257
304
|
return { title: `Run MCP Tool: ${server}/${tool}`, body: typeof args === 'string' ? args : JSON.stringify(args, null, 2), reason: 'Running external MCP tool.', isDangerous: false }
|
|
258
305
|
}
|
|
259
306
|
if (approval.UserApproval) return { title: approval.UserApproval.title, body: approval.UserApproval.prompt, reason: 'The agent requested explicit approval.', isDangerous: false }
|
|
260
|
-
if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: '
|
|
307
|
+
if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: 'Type your answer below and submit to respond to the agent.', isDangerous: false }
|
|
261
308
|
return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
|
|
262
309
|
}
|
|
263
310
|
|
|
@@ -355,7 +402,7 @@ interface ChatPanelProps {
|
|
|
355
402
|
onSetAgentMode: (enabled: boolean) => void
|
|
356
403
|
onSetProvider: (provider: string) => void
|
|
357
404
|
onSelectWorkspace: () => void
|
|
358
|
-
onApproval: (approved: boolean, autoApproveSession?: boolean) => void
|
|
405
|
+
onApproval: (approved: boolean, autoApproveSession?: boolean, answer?: string) => void
|
|
359
406
|
settingsConfig: any
|
|
360
407
|
onSetModel: (model: string) => void
|
|
361
408
|
}
|
|
@@ -450,7 +497,17 @@ function renderFormattedMessage(text: string) {
|
|
|
450
497
|
</strong>
|
|
451
498
|
)
|
|
452
499
|
}
|
|
453
|
-
|
|
500
|
+
const mentionParts = boldPart.split(/(@[\w\-\.\/]+)/g)
|
|
501
|
+
return mentionParts.map((mentionPart, mentionIndex) => {
|
|
502
|
+
if (mentionIndex % 2 === 1) {
|
|
503
|
+
return (
|
|
504
|
+
<span key={`mention-${mentionIndex}`} className="chat-mention">
|
|
505
|
+
{mentionPart}
|
|
506
|
+
</span>
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
return mentionPart
|
|
510
|
+
})
|
|
454
511
|
})
|
|
455
512
|
})
|
|
456
513
|
}
|
|
@@ -977,6 +1034,34 @@ export default function ChatPanel({
|
|
|
977
1034
|
const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
|
|
978
1035
|
const [toolMenuOpen, setToolMenuOpen] = useState(false)
|
|
979
1036
|
const [elapsedSeconds, setElapsedSeconds] = useState(0)
|
|
1037
|
+
const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
|
|
1038
|
+
const [askAnswer, setAskAnswer] = useState('')
|
|
1039
|
+
|
|
1040
|
+
useEffect(() => {
|
|
1041
|
+
setAskAnswer('')
|
|
1042
|
+
}, [pendingApproval])
|
|
1043
|
+
|
|
1044
|
+
useEffect(() => {
|
|
1045
|
+
const fetchOllamaModels = async () => {
|
|
1046
|
+
if (status?.activeProvider !== 'ollama') return;
|
|
1047
|
+
const host = settingsConfig?.ollamaHost || 'http://localhost:11434';
|
|
1048
|
+
const cleanHost = host.endsWith('/') ? host.slice(0, -1) : host;
|
|
1049
|
+
try {
|
|
1050
|
+
const res = await fetch(`${cleanHost}/api/tags`);
|
|
1051
|
+
if (res.ok) {
|
|
1052
|
+
const data = await res.json();
|
|
1053
|
+
if (data && Array.isArray(data.models)) {
|
|
1054
|
+
setDynamicOllamaModels(data.models.map((m: any) => m.name));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
// fallback to default if fetch fails
|
|
1060
|
+
}
|
|
1061
|
+
setDynamicOllamaModels(OLLAMA_MODELS);
|
|
1062
|
+
}
|
|
1063
|
+
fetchOllamaModels();
|
|
1064
|
+
}, [status?.activeProvider, settingsConfig?.ollamaHost])
|
|
980
1065
|
|
|
981
1066
|
useEffect(() => {
|
|
982
1067
|
if (!sending) {
|
|
@@ -1037,7 +1122,7 @@ export default function ChatPanel({
|
|
|
1037
1122
|
case 'local_openai':
|
|
1038
1123
|
return LOCAL_MODELS
|
|
1039
1124
|
case 'ollama':
|
|
1040
|
-
return
|
|
1125
|
+
return dynamicOllamaModels
|
|
1041
1126
|
default:
|
|
1042
1127
|
return []
|
|
1043
1128
|
}
|
|
@@ -1473,7 +1558,7 @@ export default function ChatPanel({
|
|
|
1473
1558
|
const appendWorkspaceReference = (reference: string) => {
|
|
1474
1559
|
const trimmed = reference.trim()
|
|
1475
1560
|
if (!trimmed) return
|
|
1476
|
-
onSetMessage(message.trim() ? `${message.trimEnd()} ${trimmed}` : trimmed)
|
|
1561
|
+
onSetMessage(message.trim() ? `${message.trimEnd()} ${trimmed} ` : `${trimmed} `)
|
|
1477
1562
|
}
|
|
1478
1563
|
const handleWorkspaceDrop = (event: DragEvent<HTMLElement>) => {
|
|
1479
1564
|
const reference =
|
|
@@ -1502,7 +1587,7 @@ export default function ChatPanel({
|
|
|
1502
1587
|
</button>
|
|
1503
1588
|
{isOpen && (
|
|
1504
1589
|
<div className="agent-activity-card agent-activity-card-history">
|
|
1505
|
-
{
|
|
1590
|
+
<AgentActivityTable activityView={activityView} />
|
|
1506
1591
|
</div>
|
|
1507
1592
|
)}
|
|
1508
1593
|
</div>
|
|
@@ -1701,6 +1786,12 @@ export default function ChatPanel({
|
|
|
1701
1786
|
|
|
1702
1787
|
return (
|
|
1703
1788
|
<section className={`conversation-panel ${isEmptyChat ? 'is-empty' : ''}`}>
|
|
1789
|
+
<div className="chat-header">
|
|
1790
|
+
<div className="chat-header-title">
|
|
1791
|
+
<img src="./assets/icon.png" alt="Logo" className="chat-header-logo" />
|
|
1792
|
+
<span>Mint Agent</span>
|
|
1793
|
+
</div>
|
|
1794
|
+
</div>
|
|
1704
1795
|
<div className="chat-container">
|
|
1705
1796
|
{interactions.map((interaction) => {
|
|
1706
1797
|
const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
|
|
@@ -1777,7 +1868,7 @@ export default function ChatPanel({
|
|
|
1777
1868
|
{pendingApproval ? 'Waiting for approval' : 'Working'}
|
|
1778
1869
|
</span>
|
|
1779
1870
|
</div>
|
|
1780
|
-
{
|
|
1871
|
+
<AgentActivityTable activityView={agentActivities} />
|
|
1781
1872
|
</div>
|
|
1782
1873
|
</div>
|
|
1783
1874
|
)}
|
|
@@ -1847,6 +1938,7 @@ export default function ChatPanel({
|
|
|
1847
1938
|
const writeFile = pendingApproval.approval?.WriteFile
|
|
1848
1939
|
const applyPatch = pendingApproval.approval?.ApplyPatch
|
|
1849
1940
|
const diffText = writeFile?.diff || applyPatch?.diff
|
|
1941
|
+
const isAskUser = !!pendingApproval.approval?.AskUser
|
|
1850
1942
|
|
|
1851
1943
|
return (
|
|
1852
1944
|
<div className="message ai-message" style={{ width: '100%' }}>
|
|
@@ -1863,12 +1955,54 @@ export default function ChatPanel({
|
|
|
1863
1955
|
) : (
|
|
1864
1956
|
details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
|
|
1865
1957
|
)}
|
|
1958
|
+
{isAskUser && (
|
|
1959
|
+
<div className="approval-card-input-container" style={{ marginTop: '12px', width: '100%' }}>
|
|
1960
|
+
<textarea
|
|
1961
|
+
style={{
|
|
1962
|
+
width: '100%',
|
|
1963
|
+
minHeight: '70px',
|
|
1964
|
+
padding: '10px',
|
|
1965
|
+
background: 'rgba(0, 0, 0, 0.2)',
|
|
1966
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
1967
|
+
borderRadius: '6px',
|
|
1968
|
+
color: 'var(--text-main, #f8fafc)',
|
|
1969
|
+
fontSize: '0.9rem',
|
|
1970
|
+
fontFamily: 'inherit',
|
|
1971
|
+
resize: 'vertical',
|
|
1972
|
+
outline: 'none',
|
|
1973
|
+
}}
|
|
1974
|
+
placeholder="Type your answer here..."
|
|
1975
|
+
value={askAnswer}
|
|
1976
|
+
onChange={(e) => setAskAnswer(e.target.value)}
|
|
1977
|
+
/>
|
|
1978
|
+
</div>
|
|
1979
|
+
)}
|
|
1866
1980
|
</div>
|
|
1867
|
-
|
|
1868
|
-
<
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1981
|
+
{isAskUser ? (
|
|
1982
|
+
<div className="approval-card-actions">
|
|
1983
|
+
<button
|
|
1984
|
+
type="button"
|
|
1985
|
+
className="approval-btn approval-btn-approve"
|
|
1986
|
+
disabled={!askAnswer.trim()}
|
|
1987
|
+
onClick={() => onApproval(true, false, askAnswer)}
|
|
1988
|
+
>
|
|
1989
|
+
Submit Answer
|
|
1990
|
+
</button>
|
|
1991
|
+
<button
|
|
1992
|
+
type="button"
|
|
1993
|
+
className="approval-btn approval-btn-cancel"
|
|
1994
|
+
onClick={() => onApproval(false)}
|
|
1995
|
+
>
|
|
1996
|
+
Decline
|
|
1997
|
+
</button>
|
|
1998
|
+
</div>
|
|
1999
|
+
) : (
|
|
2000
|
+
<div className="approval-card-actions">
|
|
2001
|
+
<button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
|
|
2002
|
+
<button type="button" className="approval-btn" style={{ backgroundColor: 'rgba(16, 185, 129, 0.22)', borderColor: 'rgba(16, 185, 129, 0.4)', color: '#a7f3d0' }} onClick={() => onApproval(true, true)}>Approve this session</button>
|
|
2003
|
+
<button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
|
|
2004
|
+
</div>
|
|
2005
|
+
)}
|
|
1872
2006
|
</div>
|
|
1873
2007
|
</div>
|
|
1874
2008
|
</div>
|
|
@@ -2048,7 +2182,7 @@ export default function ChatPanel({
|
|
|
2048
2182
|
return <option key={provider} value={provider}>{displayName}</option>
|
|
2049
2183
|
})}
|
|
2050
2184
|
</select>
|
|
2051
|
-
{availableModels.length > 0 && (
|
|
2185
|
+
{(availableModels.length > 0 || activeModel) && (
|
|
2052
2186
|
<select
|
|
2053
2187
|
value={activeModel}
|
|
2054
2188
|
onChange={(event) => onSetModel(event.target.value)}
|
|
@@ -2106,6 +2240,9 @@ export default function ChatPanel({
|
|
|
2106
2240
|
</button>
|
|
2107
2241
|
</form>
|
|
2108
2242
|
</div>
|
|
2243
|
+
<p className="input-disclaimer">
|
|
2244
|
+
Mint Agent is an AI gateway. Responses via third-party APIs. Verify critical info.
|
|
2245
|
+
</p>
|
|
2109
2246
|
</section>
|
|
2110
2247
|
)
|
|
2111
2248
|
}
|
|
@@ -16,6 +16,7 @@ interface ImageStudioPanelProps {
|
|
|
16
16
|
view: string
|
|
17
17
|
onRefreshPictures?: () => void
|
|
18
18
|
onSendToChat?: (dataUri: string, prompt: string) => void
|
|
19
|
+
onToggleMobileSidebar?: () => void
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const ASPECT_OPTIONS: { value: AspectRatio; label: string; icon: string }[] = [
|
|
@@ -85,7 +86,7 @@ function SkeletonCard() {
|
|
|
85
86
|
)
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat }: ImageStudioPanelProps) {
|
|
89
|
+
export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat, onToggleMobileSidebar }: ImageStudioPanelProps) {
|
|
89
90
|
const [prompt, setPrompt] = useState('')
|
|
90
91
|
const [negativePrompt, setNegativePrompt] = useState('')
|
|
91
92
|
const [showNegative, setShowNegative] = useState(false)
|
|
@@ -206,6 +207,15 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
|
|
|
206
207
|
{/* Header */}
|
|
207
208
|
<header className="img-studio-header">
|
|
208
209
|
<div className="img-studio-header-title">
|
|
210
|
+
<button
|
|
211
|
+
className="mobile-menu-btn"
|
|
212
|
+
type="button"
|
|
213
|
+
onClick={onToggleMobileSidebar}
|
|
214
|
+
aria-label="Toggle menu"
|
|
215
|
+
style={{ marginRight: '8px' }}
|
|
216
|
+
>
|
|
217
|
+
☰
|
|
218
|
+
</button>
|
|
209
219
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
210
220
|
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
|
211
221
|
</svg>
|
|
@@ -268,6 +268,7 @@ export default function MintDashboard() {
|
|
|
268
268
|
const [chatSessions, setChatSessions] = useState<ChatSession[]>([])
|
|
269
269
|
const chatEnd = useRef<HTMLDivElement | null>(null)
|
|
270
270
|
const startupReady = (dashboardDataReady && modelReady) || startupTimedOut
|
|
271
|
+
const [proactiveSuggestion, setProactiveSuggestion] = useState<any>(null)
|
|
271
272
|
|
|
272
273
|
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
|
273
274
|
const [searchQuery, setSearchQuery] = useState('')
|
|
@@ -419,10 +420,14 @@ export default function MintDashboard() {
|
|
|
419
420
|
setPendingApproval(event.payload)
|
|
420
421
|
}
|
|
421
422
|
})
|
|
423
|
+
const unlistenProactive = window.api.onProactiveSuggestion?.((suggestion: any) => {
|
|
424
|
+
setProactiveSuggestion(suggestion)
|
|
425
|
+
})
|
|
422
426
|
return () => {
|
|
423
427
|
unlistenPromise?.then?.((unlisten) => unlisten?.())
|
|
424
428
|
unlistenSpotlight?.then?.((unlisten) => unlisten?.())
|
|
425
429
|
unlistenVision?.then?.((unlisten) => unlisten?.())
|
|
430
|
+
unlistenProactive?.then?.((unlisten) => unlisten?.())
|
|
426
431
|
}
|
|
427
432
|
}, [])
|
|
428
433
|
|
|
@@ -502,14 +507,14 @@ export default function MintDashboard() {
|
|
|
502
507
|
}
|
|
503
508
|
}, [sending])
|
|
504
509
|
|
|
505
|
-
async function handleApproval(approved: boolean, autoApproveSession = false) {
|
|
510
|
+
async function handleApproval(approved: boolean, autoApproveSession = false, answer?: string) {
|
|
506
511
|
if (!pendingApproval) return
|
|
507
512
|
try {
|
|
508
513
|
if (autoApproveSession) {
|
|
509
514
|
sessionAutoApprovedRef.current = true
|
|
510
515
|
setSessionAutoApproved(true)
|
|
511
516
|
}
|
|
512
|
-
await submitToolApproval(pendingApproval.token, approved)
|
|
517
|
+
await submitToolApproval(pendingApproval.token, approved, answer)
|
|
513
518
|
} catch (reason) {
|
|
514
519
|
setError(errorMessage(reason))
|
|
515
520
|
} finally {
|
|
@@ -608,6 +613,17 @@ export default function MintDashboard() {
|
|
|
608
613
|
}
|
|
609
614
|
}
|
|
610
615
|
|
|
616
|
+
const handleProactiveAction = async (action: any) => {
|
|
617
|
+
setProactiveSuggestion(null)
|
|
618
|
+
if (action && action.type !== 'none') {
|
|
619
|
+
try {
|
|
620
|
+
await window.api.executeProactiveAction(action)
|
|
621
|
+
} catch (err) {
|
|
622
|
+
setError(String(err))
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
611
627
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
612
628
|
event.preventDefault()
|
|
613
629
|
const trimmed = message.trim()
|
|
@@ -1018,6 +1034,28 @@ export default function MintDashboard() {
|
|
|
1018
1034
|
onSetSearchOpen={setIsSearchOpen}
|
|
1019
1035
|
/>
|
|
1020
1036
|
<main className={`assistant-workspace ${layoutPreset === 'chat-wide' ? 'layout-chat-wide' : 'layout-model-wide'} ${modelVisible || view === 'workspace' ? '' : 'model-hidden'} ${view === 'workspace' ? 'workspace-open' : ''}`}>
|
|
1037
|
+
{proactiveSuggestion && (
|
|
1038
|
+
<div className="proactive-bar" style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100 }}>
|
|
1039
|
+
<div className="proactive-header">
|
|
1040
|
+
<span className="proactive-icon">✨</span>
|
|
1041
|
+
<div className="proactive-message">{proactiveSuggestion.message}</div>
|
|
1042
|
+
<button className="proactive-dismiss-btn" onClick={() => setProactiveSuggestion(null)}>
|
|
1043
|
+
Dismiss
|
|
1044
|
+
</button>
|
|
1045
|
+
</div>
|
|
1046
|
+
<div className="proactive-chips">
|
|
1047
|
+
{proactiveSuggestion.suggestions?.map((sug: any, i: number) => (
|
|
1048
|
+
<button
|
|
1049
|
+
key={i}
|
|
1050
|
+
className="suggestion-chip"
|
|
1051
|
+
onClick={() => handleProactiveAction(sug.action)}
|
|
1052
|
+
>
|
|
1053
|
+
{sug.label}
|
|
1054
|
+
</button>
|
|
1055
|
+
))}
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
)}
|
|
1021
1059
|
{view === 'workspace' && (
|
|
1022
1060
|
<WorkspacePanel
|
|
1023
1061
|
agentMode={agentMode}
|