@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.
Files changed (66) 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 +40 -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 +71 -4
  11. package/crates/mint-core/src/browser.rs +401 -0
  12. package/crates/mint-core/src/chat.rs +79 -0
  13. package/crates/mint-core/src/config.rs +69 -1
  14. package/crates/mint-core/src/lib.rs +6 -1
  15. package/crates/mint-core/src/orchestration.rs +137 -1
  16. package/crates/mint-core/src/skills.rs +113 -4
  17. package/crates/mint-core/src/workflows.rs +9 -22
  18. package/package.json +2 -1
  19. package/src/renderer/src/App.tsx +1 -1
  20. package/src/renderer/src/components/ChatPanel.tsx +197 -47
  21. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  22. package/src/renderer/src/components/ImageStudioPanel.tsx +18 -4
  23. package/src/renderer/src/components/MintDashboard.tsx +239 -7
  24. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  25. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  26. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  27. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  28. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  29. package/src/renderer/src/components/Settings/PluginsTab.tsx +632 -0
  30. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  31. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  32. package/src/renderer/src/components/SettingsWindow.tsx +175 -970
  33. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  34. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  35. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  36. package/src/renderer/src/css/image-studio.css +15 -4
  37. package/src/renderer/src/css/settings.css +75 -6
  38. package/src/renderer/src/css/styles.css +419 -49
  39. package/src/renderer/src/css/workflows.css +542 -0
  40. package/src/renderer/src/env.d.ts +2 -0
  41. package/src/renderer/src/index.css +13 -1
  42. package/src/renderer/src/tauri.ts +140 -2
  43. package/src/renderer/src-web/components/ChatPanel.tsx +190 -46
  44. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  45. package/src/renderer/src-web/components/ImageStudioPanel.tsx +18 -4
  46. package/src/renderer/src-web/components/MintDashboard.tsx +197 -3
  47. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  48. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  49. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  50. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  51. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  52. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +632 -0
  53. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  54. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  55. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -976
  56. package/src/renderer/src-web/css/image-studio.css +15 -4
  57. package/src/renderer/src-web/css/settings.css +75 -6
  58. package/src/renderer/src-web/css/styles.css +394 -50
  59. package/src/renderer/src-web/env.d.ts +2 -0
  60. package/src/renderer/src-web/index.css +12 -1
  61. package/src/renderer/src-web/tauri.ts +60 -2
  62. package/src-tauri/src/browser.rs +1 -141
  63. package/src-tauri/src/headless.rs +3 -2
  64. package/src-tauri/src/lib.rs +195 -111
  65. package/src-tauri/src/workflows.rs +56 -4
  66. package/src-tauri/tauri.conf.json +1 -1
@@ -128,6 +128,28 @@ export async function getRuntimeStatus(): Promise<RuntimeStatus> {
128
128
  return invoke<RuntimeStatus>('get_runtime_status')
129
129
  }
130
130
 
131
+ export interface DetectedTools {
132
+ docker: boolean
133
+ git: boolean
134
+ gh: boolean
135
+ node: boolean
136
+ }
137
+
138
+ export async function detectSystemTools(): Promise<DetectedTools> {
139
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
140
+ const API_BASE = "http://localhost:3000/api";
141
+ try {
142
+ const res = await fetch(`${API_BASE}/detect-tools`);
143
+ return await res.json();
144
+ } catch (e) {
145
+ console.error("Failed to detect tools from local server:", e);
146
+ return { docker: false, git: false, gh: false, node: false };
147
+ }
148
+ }
149
+ const { invoke } = await import('@tauri-apps/api/core')
150
+ return invoke<DetectedTools>('detect_system_tools')
151
+ }
152
+
131
153
  export async function sendChatMessage(
132
154
  message: string,
133
155
  imageDataUri?: string | null,
@@ -282,6 +304,30 @@ export async function getRecentInteractions(limit = 50, chatId?: string | null):
282
304
  return invoke<InteractionMemory[]>('get_recent_interactions', { limit, chatId })
283
305
  }
284
306
 
307
+ export async function saveSystemInteraction(
308
+ chatId: string,
309
+ userText: string,
310
+ provider: string,
311
+ model: string,
312
+ ): Promise<any> {
313
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
314
+ const API_BASE = "http://localhost:3000/api";
315
+ try {
316
+ const res = await fetch(`${API_BASE}/interactions`, {
317
+ method: 'POST',
318
+ headers: { 'Content-Type': 'application/json' },
319
+ body: JSON.stringify({ chatId, userText, provider, model }),
320
+ });
321
+ return await res.json();
322
+ } catch (e) {
323
+ console.error("Failed to save system interaction on local server:", e);
324
+ return { success: false };
325
+ }
326
+ }
327
+ const { invoke } = await import('@tauri-apps/api/core')
328
+ return invoke('save_system_interaction', { chatId, userText, provider, model })
329
+ }
330
+
285
331
  export async function saveInteractionAgentActivity(
286
332
  interactionId: number,
287
333
  activity: AgentProgress[],
@@ -396,6 +442,72 @@ export async function setProfileValue(key: string, value: string): Promise<boole
396
442
  return invoke<void>('set_profile_value', { key, value }).then(() => true).catch(() => false)
397
443
  }
398
444
 
445
+ export interface LearnedSkill {
446
+ id: number
447
+ name: string
448
+ sourcePath: string
449
+ content: string
450
+ updatedAt: string
451
+ location?: string
452
+ }
453
+
454
+ export async function listLearnedSkills(workspacePath?: string): Promise<LearnedSkill[]> {
455
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
456
+ try {
457
+ const val = await getProfileValue('learned-skills-web-mock')
458
+ if (val) {
459
+ return JSON.parse(val)
460
+ }
461
+ return []
462
+ } catch (e) {
463
+ console.error("Failed to load web mock skills:", e)
464
+ return []
465
+ }
466
+ }
467
+ const { invoke } = await import('@tauri-apps/api/core')
468
+ return invoke<LearnedSkill[]>('list_learned_skills', { workspacePath })
469
+ }
470
+
471
+ export async function addLearnedSkill(name: string, content: string): Promise<LearnedSkill> {
472
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
473
+ try {
474
+ const list = await listLearnedSkills()
475
+ const newSkill: LearnedSkill = {
476
+ id: Date.now(),
477
+ name,
478
+ sourcePath: 'ui_manual',
479
+ content,
480
+ updatedAt: new Date().toISOString()
481
+ }
482
+ list.push(newSkill)
483
+ await setProfileValue('learned-skills-web-mock', JSON.stringify(list))
484
+ return newSkill
485
+ } catch (e) {
486
+ console.error("Failed to add web mock skill:", e)
487
+ throw e
488
+ }
489
+ }
490
+ const { invoke } = await import('@tauri-apps/api/core')
491
+ return invoke<LearnedSkill>('add_learned_skill', { name, content })
492
+ }
493
+
494
+ export async function deleteLearnedSkill(name: string): Promise<number> {
495
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
496
+ try {
497
+ const list = await listLearnedSkills()
498
+ const filtered = list.filter(s => s.name !== name)
499
+ const deletedCount = list.length - filtered.length
500
+ await setProfileValue('learned-skills-web-mock', JSON.stringify(filtered))
501
+ return deletedCount
502
+ } catch (e) {
503
+ console.error("Failed to delete web mock skill:", e)
504
+ return 0
505
+ }
506
+ }
507
+ const { invoke } = await import('@tauri-apps/api/core')
508
+ return invoke<number>('delete_learned_skill', { name })
509
+ }
510
+
399
511
  export async function clearChatHistory(chatId?: string | null): Promise<number> {
400
512
  if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
401
513
  const API_BASE = "http://localhost:3000/api";
@@ -558,6 +670,24 @@ export async function getWorkspaceTree(path?: string | null): Promise<WorkspaceT
558
670
  return invoke<WorkspaceTreeEntry>('get_workspace_tree', { path })
559
671
  }
560
672
 
673
+ export async function createWorkspaceFile(path: string): Promise<void> {
674
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) return
675
+ const { invoke } = await import('@tauri-apps/api/core')
676
+ return invoke('create_workspace_file', { path })
677
+ }
678
+
679
+ export async function createWorkspaceFolder(path: string): Promise<void> {
680
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) return
681
+ const { invoke } = await import('@tauri-apps/api/core')
682
+ return invoke('create_workspace_folder', { path })
683
+ }
684
+
685
+ export async function deleteWorkspaceItem(path: string): Promise<void> {
686
+ if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) return
687
+ const { invoke } = await import('@tauri-apps/api/core')
688
+ return invoke('delete_workspace_item', { path })
689
+ }
690
+
561
691
  export async function selectWorkspaceDirectory(): Promise<string | null> {
562
692
  if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
563
693
  return null
@@ -567,12 +697,12 @@ export async function selectWorkspaceDirectory(): Promise<string | null> {
567
697
  return selected?.trim() || null
568
698
  }
569
699
 
570
- export async function submitToolApproval(token: string, approved: boolean): Promise<void> {
700
+ export async function submitToolApproval(token: string, approved: boolean, answer?: string): Promise<void> {
571
701
  if (typeof window === 'undefined' || !(window as any).__TAURI_INTERNALS__) {
572
702
  return;
573
703
  }
574
704
  const { invoke } = await import('@tauri-apps/api/core')
575
- return invoke('submit_tool_approval', { token, approved })
705
+ return invoke('submit_tool_approval', { token, approved, answer })
576
706
  }
577
707
 
578
708
  export async function proposeCodeEdits(root: string, edits: CodeEdit[]): Promise<CodeEditProposal> {
@@ -889,6 +1019,10 @@ export function installTauriAdapters() {
889
1019
  const { invoke } = await import('@tauri-apps/api/core')
890
1020
  return invoke('reload_custom_workflows')
891
1021
  },
1022
+ saveCustomWorkflows: async (workflows) => {
1023
+ const { invoke } = await import('@tauri-apps/api/core')
1024
+ return invoke('save_custom_workflows', { workflows })
1025
+ },
892
1026
  }
893
1027
 
894
1028
  window.spotlightAPI = {
@@ -1015,6 +1149,10 @@ export function installTauriAdapters() {
1015
1149
  const { invoke } = await import('@tauri-apps/api/core')
1016
1150
  return invoke('open_window', { kind: 'settings' })
1017
1151
  },
1152
+ openWorkflows: async () => {
1153
+ const { invoke } = await import('@tauri-apps/api/core')
1154
+ return invoke('open_window', { kind: 'workflows' })
1155
+ },
1018
1156
  readClipboard: () => navigator.clipboard.readText(),
1019
1157
  writeClipboard: (text) => navigator.clipboard.writeText(text),
1020
1158
  getSystemInfo: async () => ({ backend: 'rust' }),
@@ -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
 
@@ -353,7 +400,7 @@ interface ChatPanelProps {
353
400
  onSetSmartContext: (enabled: boolean) => void
354
401
  onSetAgentMode: (enabled: boolean) => void
355
402
  onSetProvider: (provider: string) => void
356
- onApproval: (approved: boolean, autoApproveSession?: boolean) => void
403
+ onApproval: (approved: boolean, autoApproveSession?: boolean, answer?: string) => void
357
404
  onToggleMobileSidebar: () => void
358
405
  settingsConfig: any
359
406
  onSetModel: (model: string) => void
@@ -449,7 +496,17 @@ function renderFormattedMessage(text: string) {
449
496
  </strong>
450
497
  )
451
498
  }
452
- return boldPart
499
+ const mentionParts = boldPart.split(/(@[\w\-\.\/]+)/g)
500
+ return mentionParts.map((mentionPart, mentionIndex) => {
501
+ if (mentionIndex % 2 === 1) {
502
+ return (
503
+ <span key={`mention-${mentionIndex}`} className="chat-mention">
504
+ {mentionPart}
505
+ </span>
506
+ )
507
+ }
508
+ return mentionPart
509
+ })
453
510
  })
454
511
  })
455
512
  }
@@ -975,6 +1032,34 @@ export default function ChatPanel({
975
1032
  const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
976
1033
  const [toolMenuOpen, setToolMenuOpen] = useState(false)
977
1034
  const toolMenuRef = useRef<HTMLDivElement | null>(null)
1035
+ const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
1036
+ const [askAnswer, setAskAnswer] = useState('')
1037
+
1038
+ useEffect(() => {
1039
+ setAskAnswer('')
1040
+ }, [pendingApproval])
1041
+
1042
+ useEffect(() => {
1043
+ const fetchOllamaModels = async () => {
1044
+ if (status?.activeProvider !== 'ollama') return;
1045
+ const host = settingsConfig?.ollamaHost || 'http://localhost:11434';
1046
+ const cleanHost = host.endsWith('/') ? host.slice(0, -1) : host;
1047
+ try {
1048
+ const res = await fetch(`${cleanHost}/api/tags`);
1049
+ if (res.ok) {
1050
+ const data = await res.json();
1051
+ if (data && Array.isArray(data.models)) {
1052
+ setDynamicOllamaModels(data.models.map((m: any) => m.name));
1053
+ return;
1054
+ }
1055
+ }
1056
+ } catch (err) {
1057
+ // fallback to default if fetch fails
1058
+ }
1059
+ setDynamicOllamaModels(OLLAMA_MODELS);
1060
+ }
1061
+ fetchOllamaModels();
1062
+ }, [status?.activeProvider, settingsConfig?.ollamaHost])
978
1063
  const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
979
1064
  const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
980
1065
 
@@ -995,7 +1080,7 @@ export default function ChatPanel({
995
1080
  case 'local_openai':
996
1081
  return LOCAL_MODELS
997
1082
  case 'ollama':
998
- return OLLAMA_MODELS
1083
+ return dynamicOllamaModels
999
1084
  default:
1000
1085
  return []
1001
1086
  }
@@ -1543,7 +1628,7 @@ export default function ChatPanel({
1543
1628
  </button>
1544
1629
  {isOpen && (
1545
1630
  <div className="agent-activity-card agent-activity-card-history">
1546
- {renderAgentActivityTable(activityView)}
1631
+ <AgentActivityTable activityView={activityView} />
1547
1632
  </div>
1548
1633
  )}
1549
1634
  </div>
@@ -1798,21 +1883,33 @@ export default function ChatPanel({
1798
1883
  )}
1799
1884
 
1800
1885
  <div className="chat-container">
1801
- {interactions.map((interaction) => (
1802
- <div key={interaction.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
1803
- {interaction.isSystemEvent ? (
1804
- <div className="system-event" style={{ background: 'rgba(16, 185, 129, 0.06)', border: '1px solid rgba(16, 185, 129, 0.2)', borderRadius: '8px', padding: '10px 14px', color: '#a7f3d0', fontSize: '0.82rem', lineHeight: '1.45', alignSelf: 'stretch' }}>
1805
- {interaction.userText}
1806
- </div>
1807
- ) : interaction.userText && (
1808
- <div className="message user-message">
1809
- <div className="bubble-wrapper">
1810
- <div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
1811
- <div className="message-time"><span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>
1886
+ {interactions.map((interaction) => {
1887
+ const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
1888
+ if (isSystemEvent) {
1889
+ return (
1890
+ <div key={interaction.id} className="system-event-divider">
1891
+ <div className="system-event-line" />
1892
+ <div className="system-event-pill">
1893
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
1894
+ <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" />
1895
+ </svg>
1896
+ <span>{interaction.userText}</span>
1812
1897
  </div>
1898
+ <div className="system-event-line" />
1813
1899
  </div>
1814
- )}
1815
- <div className="message ai-message">
1900
+ );
1901
+ }
1902
+ return (
1903
+ <div key={interaction.id} style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
1904
+ {interaction.userText && (
1905
+ <div className="message user-message">
1906
+ <div className="bubble-wrapper">
1907
+ <div className="message-bubble">{renderFormattedMessage(interaction.userText)}</div>
1908
+ <div className="message-time"><span>{new Date(interaction.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span></div>
1909
+ </div>
1910
+ </div>
1911
+ )}
1912
+ <div className="message ai-message">
1816
1913
  <div className="bubble-wrapper">
1817
1914
  {renderCompletedActivity(interaction)}
1818
1915
  {renderFileChanges(interaction)}
@@ -1858,7 +1955,8 @@ export default function ChatPanel({
1858
1955
  </div>
1859
1956
  </div>
1860
1957
  </div>
1861
- ))}
1958
+ );
1959
+ })}
1862
1960
 
1863
1961
  {sending && (
1864
1962
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
@@ -1872,7 +1970,7 @@ export default function ChatPanel({
1872
1970
  {pendingApproval ? 'Waiting for approval' : 'Working'}
1873
1971
  </span>
1874
1972
  </div>
1875
- {renderAgentActivityTable(agentActivities)}
1973
+ <AgentActivityTable activityView={agentActivities} />
1876
1974
  </div>
1877
1975
  </div>
1878
1976
  )}
@@ -1954,6 +2052,7 @@ export default function ChatPanel({
1954
2052
  const writeFile = pendingApproval.approval?.WriteFile
1955
2053
  const applyPatch = pendingApproval.approval?.ApplyPatch
1956
2054
  const diffText = writeFile?.diff || applyPatch?.diff
2055
+ const isAskUser = !!pendingApproval.approval?.AskUser
1957
2056
 
1958
2057
  return (
1959
2058
  <div className="message ai-message" style={{ width: '100%' }}>
@@ -1970,12 +2069,54 @@ export default function ChatPanel({
1970
2069
  ) : (
1971
2070
  details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
1972
2071
  )}
2072
+ {isAskUser && (
2073
+ <div className="approval-card-input-container" style={{ marginTop: '12px', width: '100%' }}>
2074
+ <textarea
2075
+ style={{
2076
+ width: '100%',
2077
+ minHeight: '70px',
2078
+ padding: '10px',
2079
+ background: 'rgba(0, 0, 0, 0.2)',
2080
+ border: '1px solid rgba(255, 255, 255, 0.1)',
2081
+ borderRadius: '6px',
2082
+ color: 'var(--text-main, #f8fafc)',
2083
+ fontSize: '0.9rem',
2084
+ fontFamily: 'inherit',
2085
+ resize: 'vertical',
2086
+ outline: 'none',
2087
+ }}
2088
+ placeholder="Type your answer here..."
2089
+ value={askAnswer}
2090
+ onChange={(e) => setAskAnswer(e.target.value)}
2091
+ />
2092
+ </div>
2093
+ )}
1973
2094
  </div>
1974
- <div className="approval-card-actions">
1975
- <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
1976
- <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>
1977
- <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
1978
- </div>
2095
+ {isAskUser ? (
2096
+ <div className="approval-card-actions">
2097
+ <button
2098
+ type="button"
2099
+ className="approval-btn approval-btn-approve"
2100
+ disabled={!askAnswer.trim()}
2101
+ onClick={() => onApproval(true, false, askAnswer)}
2102
+ >
2103
+ Submit Answer
2104
+ </button>
2105
+ <button
2106
+ type="button"
2107
+ className="approval-btn approval-btn-cancel"
2108
+ onClick={() => onApproval(false)}
2109
+ >
2110
+ Decline
2111
+ </button>
2112
+ </div>
2113
+ ) : (
2114
+ <div className="approval-card-actions">
2115
+ <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
2116
+ <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>
2117
+ <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
2118
+ </div>
2119
+ )}
1979
2120
  </div>
1980
2121
  </div>
1981
2122
  </div>
@@ -2121,7 +2262,7 @@ export default function ChatPanel({
2121
2262
  return <option key={provider} value={provider}>{displayName}</option>
2122
2263
  })}
2123
2264
  </select>
2124
- {availableModels.length > 0 && (
2265
+ {(availableModels.length > 0 || activeModel) && (
2125
2266
  <select
2126
2267
  value={activeModel}
2127
2268
  onChange={(event) => onSetModel(event.target.value)}
@@ -2179,6 +2320,9 @@ export default function ChatPanel({
2179
2320
  </button>
2180
2321
  </form>
2181
2322
  </div>
2323
+ <p className="input-disclaimer">
2324
+ Mint Agent is an AI gateway. Responses via third-party APIs. Verify critical info.
2325
+ </p>
2182
2326
  </section>
2183
2327
  )
2184
2328
  }
@@ -1,11 +1,13 @@
1
1
  import { useState } from 'react'
2
2
 
3
- export type DashboardView = 'chat' | 'pictures' | 'model' | 'imagine'
3
+ export type DashboardView = 'chat' | 'pictures' | 'model' | 'imagine' | 'workflows'
4
4
 
5
5
  interface ChatSessionItem {
6
6
  id: string
7
7
  title: string
8
8
  kind: string
9
+ createdAt?: string
10
+ updatedAt?: string
9
11
  }
10
12
 
11
13
  interface DashboardSidebarProps {
@@ -20,6 +22,8 @@ interface DashboardSidebarProps {
20
22
  onDeleteConversation: (id: string) => void
21
23
  onRenameConversation?: (id: string, newTitle: string) => void
22
24
  onSetView: (view: DashboardView) => void
25
+ isSearchOpen: boolean
26
+ onSetSearchOpen: (open: boolean) => void
23
27
  }
24
28
 
25
29
  export default function DashboardSidebar({
@@ -34,6 +38,8 @@ export default function DashboardSidebar({
34
38
  onDeleteConversation,
35
39
  onRenameConversation,
36
40
  onSetView,
41
+ isSearchOpen,
42
+ onSetSearchOpen,
37
43
  }: DashboardSidebarProps) {
38
44
  const [editingSessionId, setEditingSessionId] = useState<string | null>(null)
39
45
  const [editTitleValue, setEditTitleValue] = useState('')
@@ -77,6 +83,16 @@ export default function DashboardSidebar({
77
83
  <span>New Chat</span>
78
84
  </button>
79
85
 
86
+ <button className="sidebar-top-action sidebar-search-btn" onClick={() => onSetSearchOpen(true)}>
87
+ <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
88
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
89
+ <circle cx="11" cy="11" r="8"></circle>
90
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
91
+ </svg>
92
+ </span>
93
+ <span>Search Chats</span>
94
+ </button>
95
+
80
96
  <button className={`sidebar-top-action ${view === 'chat' ? 'is-active' : ''}`} onClick={() => onSetView('chat')}>
81
97
  <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
82
98
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
@@ -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)
@@ -193,15 +194,28 @@ export default function ImageStudioPanel({ view, onRefreshPictures, onSendToChat
193
194
  document.body.removeChild(a)
194
195
  }
195
196
 
196
- if (view !== 'imagine') return null
197
-
198
197
  const modelOptions = PROVIDER_MODELS[selectedProvider] ?? []
199
198
 
200
199
  return (
201
- <div className="img-studio" id="image-studio-panel" role="main" aria-label="Image Studio">
200
+ <div
201
+ className={`img-studio ${view === 'imagine' ? 'is-visible' : ''}`}
202
+ id="image-studio-panel"
203
+ role="main"
204
+ aria-label="Image Studio"
205
+ aria-hidden={view !== 'imagine'}
206
+ >
202
207
  {/* Header */}
203
208
  <header className="img-studio-header">
204
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>
205
219
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
206
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"/>
207
221
  </svg>