@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.
Files changed (41) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +31 -0
  5. package/Release_Note.md +95 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/crates/mint-cli/Cargo.toml +1 -0
  8. package/crates/mint-cli/src/agent.rs +142 -48
  9. package/crates/mint-cli/src/main.rs +231 -22
  10. package/crates/mint-core/src/api_server.rs +9 -0
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/config.rs +69 -1
  13. package/crates/mint-core/src/lib.rs +5 -0
  14. package/crates/mint-core/src/orchestration.rs +137 -1
  15. package/crates/mint-core/src/skills.rs +113 -4
  16. package/crates/mint-core/src/workflows.rs +1 -22
  17. package/package.json +1 -1
  18. package/src/renderer/src/components/ChatPanel.tsx +170 -33
  19. package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
  20. package/src/renderer/src/components/MintDashboard.tsx +40 -2
  21. package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
  22. package/src/renderer/src/components/SettingsWindow.tsx +1 -9
  23. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  24. package/src/renderer/src/css/image-studio.css +6 -0
  25. package/src/renderer/src/css/styles.css +64 -11
  26. package/src/renderer/src/index.css +12 -1
  27. package/src/renderer/src/tauri.ts +45 -4
  28. package/src/renderer/src-web/components/ChatPanel.tsx +163 -32
  29. package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
  30. package/src/renderer/src-web/components/MintDashboard.tsx +8 -3
  31. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
  32. package/src/renderer/src-web/components/SettingsWindow.tsx +1 -9
  33. package/src/renderer/src-web/css/image-studio.css +6 -0
  34. package/src/renderer/src-web/css/styles.css +100 -12
  35. package/src/renderer/src-web/index.css +12 -1
  36. package/src/renderer/src-web/tauri.ts +24 -2
  37. package/src-tauri/src/browser.rs +1 -141
  38. package/src-tauri/src/headless.rs +3 -2
  39. package/src-tauri/src/lib.rs +117 -26
  40. package/src-tauri/src/workflows.rs +56 -4
  41. 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
- let value = skills
59
- .into_iter()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pheem49/mint",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
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",
@@ -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 target = path || query || command || name || tool || action.replaceAll('_', ' ')
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') activities[index].label = 'Failed'
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 renderAgentActivityTable(activityView: AgentActivityView) {
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
- <div className="agent-activity-item" data-kind={activity.kind} data-state={activity.state} key={`${index}-${activity.label}-${activity.target}`}>
238
- <span className="agent-activity-label">{activity.label}</span>
239
- <span className="agent-activity-icon" aria-hidden="true" />
240
- <span className="agent-activity-text">{activity.target}</span>
241
- <span className="agent-activity-chevron" aria-hidden="true">&gt;</span>
242
- </div>
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
+ &gt;
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: 'Approve to continue without a typed answer, or cancel to decline.', isDangerous: false }
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
- return boldPart
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 OLLAMA_MODELS
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
- {renderAgentActivityTable(activityView)}
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
- {renderAgentActivityTable(agentActivities)}
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
- <div className="approval-card-actions">
1868
- <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
1869
- <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>
1870
- <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
1871
- </div>
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}