@pheem49/mint 1.5.2 → 1.5.4

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 (40) hide show
  1. package/GUIDE_TH.md +23 -11
  2. package/README.md +148 -66
  3. package/assets/Agent_Mint.png +0 -0
  4. package/assets/Settings.png +0 -0
  5. package/install.ps1 +64 -0
  6. package/install.sh +54 -0
  7. package/main.js +12 -0
  8. package/package.json +5 -3
  9. package/preload.js +4 -0
  10. package/scripts/install_linux_desktop_entry.js +48 -0
  11. package/src/AI_Brain/Gemini_API.js +231 -498
  12. package/src/AI_Brain/autonomous_brain.js +46 -19
  13. package/src/AI_Brain/headless_agent.js +21 -2
  14. package/src/AI_Brain/provider_adapter.js +358 -0
  15. package/src/Automation_Layer/file_operations.js +17 -5
  16. package/src/CLI/approval_handler.js +5 -0
  17. package/src/CLI/chat_router.js +7 -0
  18. package/src/CLI/chat_ui.js +397 -76
  19. package/src/CLI/cli_colors.js +86 -3
  20. package/src/CLI/cli_formatters.js +6 -1
  21. package/src/CLI/code_agent.js +706 -273
  22. package/src/CLI/interactive_chat.js +311 -149
  23. package/src/CLI/slash_command_handler.js +2 -2
  24. package/src/CLI/updater.js +21 -1
  25. package/src/System/config_manager.js +5 -1
  26. package/src/System/ipc_handlers.js +95 -1
  27. package/src/System/picture_store.js +109 -0
  28. package/src/System/smart_context.js +227 -0
  29. package/src/System/task_manager.js +127 -0
  30. package/src/System/tool_registry.js +13 -0
  31. package/src/System/window_manager.js +16 -8
  32. package/src/UI/live2d_manager.js +42 -8
  33. package/src/UI/preload-spotlight.js +1 -0
  34. package/src/UI/renderer.js +837 -63
  35. package/src/UI/settings.css +160 -96
  36. package/src/UI/settings.html +9 -0
  37. package/src/UI/settings.js +35 -2
  38. package/src/UI/spotlight.js +13 -9
  39. package/src/UI/styles.css +1592 -165
  40. package/privacy.txt +0 -1
@@ -6,14 +6,36 @@ const maximizeBtn = document.getElementById('maximize-btn');
6
6
  const minimizeBtn = document.getElementById('minimize-btn');
7
7
  const clearBtn = document.getElementById('clear-btn');
8
8
  const settingsBtn = document.getElementById('settings-btn');
9
+ const sidebarNewChatBtn = document.getElementById('sidebar-new-chat');
10
+ const sidebarSettingsBtn = document.getElementById('sidebar-settings');
11
+ const sidebarToggleBtn = document.getElementById('sidebar-toggle');
12
+ const appBody = document.querySelector('.app-body');
13
+ const sidebarChatBtn = document.getElementById('sidebar-chat-btn');
14
+ const sidebarPicturesBtn = document.getElementById('sidebar-pictures-btn');
15
+ const picturesLibrary = document.getElementById('pictures-library');
16
+ const picturesGrid = document.getElementById('pictures-grid');
17
+ const picturesEmpty = document.getElementById('pictures-empty');
18
+ const picturesCloseBtn = document.getElementById('pictures-close-btn');
9
19
  const micBtn = document.getElementById('mic-btn');
10
20
  const visionBtn = document.getElementById('vision-btn');
21
+ const chatProviderSelect = document.getElementById('chat-provider-select');
11
22
  const imagePreviewContainer = document.getElementById('image-preview-container');
12
23
  const imagePreview = document.getElementById('image-preview');
13
24
  const removeImageBtn = document.getElementById('remove-image-btn');
25
+ const agentModeToggle = document.getElementById('agent-mode-toggle');
14
26
  const modelMount = document.getElementById('model-mount');
15
27
  const modelShell = document.getElementById('model-shell');
16
28
  const modelStatus = document.getElementById('model-status');
29
+ const mintStatus = document.getElementById('mint-status');
30
+ const mintStatusLabel = document.getElementById('mint-status-label');
31
+ const modelActivityBadge = document.getElementById('model-activity-badge');
32
+ const startupLoading = document.getElementById('startup-loading');
33
+ const appContainer = document.querySelector('.app-container');
34
+
35
+ if (startupLoading) {
36
+ startupLoading.style.background = 'var(--bg-gradient)';
37
+ startupLoading.style.color = 'var(--text-muted)';
38
+ }
17
39
 
18
40
  // Proactive Assistant elements
19
41
  const proactiveBar = document.getElementById('proactive-bar');
@@ -28,6 +50,92 @@ let ttsVolume = 1.0;
28
50
  let ttsSpeed = 1.0;
29
51
  let ttsPitch = 1.0;
30
52
  let lastConversationLanguage = 'auto';
53
+ let mintActivityResetTimer = null;
54
+ let currentSettings = {};
55
+
56
+ const PROVIDER_PICKER_OPTIONS = [
57
+ ['gemini', 'Gemini'],
58
+ ['anthropic', 'Claude'],
59
+ ['openai', 'OpenAI'],
60
+ ['ollama', 'Ollama'],
61
+ ['huggingface', 'Hugging Face'],
62
+ ['local_openai', 'Local']
63
+ ];
64
+
65
+ function buildProviderPicker(settings = currentSettings) {
66
+ if (!chatProviderSelect) return;
67
+ chatProviderSelect.textContent = '';
68
+ PROVIDER_PICKER_OPTIONS.forEach(([value, label]) => {
69
+ const option = document.createElement('option');
70
+ option.value = value;
71
+ option.textContent = label;
72
+ chatProviderSelect.appendChild(option);
73
+ });
74
+ chatProviderSelect.value = settings.aiProvider || 'gemini';
75
+ }
76
+
77
+ function syncAgentModeToggle(settings = currentSettings) {
78
+ if (!agentModeToggle) return;
79
+ agentModeToggle.checked = settings.assistantMode === 'agent';
80
+ agentModeToggle.closest('.smart-context-control')?.classList.toggle('is-active', agentModeToggle.checked);
81
+ }
82
+
83
+ async function changeChatProvider(provider) {
84
+ if (!PROVIDER_PICKER_OPTIONS.some(([value]) => value === provider)) return;
85
+ const nextSettings = { ...currentSettings, aiProvider: provider };
86
+ chatProviderSelect.disabled = true;
87
+ try {
88
+ const result = await window.api.saveSettings(nextSettings);
89
+ if (!result || result.success !== false) {
90
+ currentSettings = nextSettings;
91
+ buildProviderPicker(currentSettings);
92
+ } else {
93
+ throw new Error(result.message || 'Unable to save provider setting');
94
+ }
95
+ } catch (error) {
96
+ console.error('Failed to change provider:', error);
97
+ buildProviderPicker(currentSettings);
98
+ setMintActivity('error');
99
+ } finally {
100
+ chatProviderSelect.disabled = false;
101
+ }
102
+ }
103
+
104
+ const MINT_ACTIVITY_STATES = {
105
+ idle: { label: 'Idle', title: 'Mint is idle' },
106
+ listening: { label: 'Listening', title: 'Mint is listening' },
107
+ thinking: { label: 'Thinking', title: 'Mint is thinking' },
108
+ speaking: { label: 'Speaking', title: 'Mint is speaking' },
109
+ error: { label: 'Error', title: 'Mint needs attention' }
110
+ };
111
+
112
+ function setMintActivity(state, options = {}) {
113
+ const normalizedState = MINT_ACTIVITY_STATES[state] ? state : 'idle';
114
+ const meta = MINT_ACTIVITY_STATES[normalizedState];
115
+ if (mintActivityResetTimer) {
116
+ clearTimeout(mintActivityResetTimer);
117
+ mintActivityResetTimer = null;
118
+ }
119
+
120
+ [mintStatus, modelActivityBadge].forEach((element) => {
121
+ if (!element) return;
122
+ element.dataset.state = normalizedState;
123
+ element.title = meta.title;
124
+ const label = element.querySelector('.mint-status-label');
125
+ if (label) label.textContent = meta.label;
126
+ });
127
+ if (mintStatusLabel) mintStatusLabel.textContent = meta.label;
128
+
129
+ if (window.api && window.api.setAiState) {
130
+ window.api.setAiState(normalizedState);
131
+ }
132
+
133
+ if (normalizedState === 'error' || options.resetAfter) {
134
+ mintActivityResetTimer = setTimeout(() => {
135
+ setMintActivity('idle');
136
+ }, options.resetAfter || 3500);
137
+ }
138
+ }
31
139
 
32
140
  function detectConversationLanguage(text) {
33
141
  const value = String(text || '');
@@ -56,8 +164,11 @@ function buildInteractionLanguageInstruction() {
56
164
  // --- Theme Loading ---
57
165
  function applyTheme(theme, accentColor, systemTextColor, config = {}) {
58
166
  document.documentElement.setAttribute('data-theme', theme || 'dark');
59
- const accent = accentColor || '#8b5cf6';
60
- const textColor = systemTextColor || '#f8fafc';
167
+ const accent = accentColor || '#8f6cf5';
168
+ const defaultTextColor = theme === 'light' ? '#0f172a' : '#e8e8ea';
169
+ const textColor = (!systemTextColor || (theme === 'light' && systemTextColor === '#f8fafc'))
170
+ ? defaultTextColor
171
+ : systemTextColor;
61
172
  document.documentElement.style.setProperty('--accent', accent);
62
173
  document.documentElement.style.setProperty('--accent-hover', lightenColor(accent, 20));
63
174
  document.documentElement.style.setProperty('--text-main', textColor);
@@ -65,19 +176,36 @@ function applyTheme(theme, accentColor, systemTextColor, config = {}) {
65
176
  // Dynamic UI Customizations
66
177
  document.documentElement.style.setProperty('--glass-blur', config.glassBlur || 'blur(16px)');
67
178
  document.body.style.fontFamily = config.fontFamily || "'Outfit', sans-serif";
179
+ document.documentElement.style.fontSize = config.fontSize || '15px';
68
180
 
69
181
  if (theme === 'custom') {
70
182
  if (config.customBgStart && config.customBgEnd) {
71
183
  const gradient = `linear-gradient(135deg, ${config.customBgStart} 0%, ${config.customBgEnd} 100%)`;
184
+ document.documentElement.style.setProperty('--bg-color', config.customBgStart);
72
185
  document.documentElement.style.setProperty('--bg-gradient', gradient);
73
186
  }
74
187
  if (config.customPanelBg) {
75
188
  const rgb = hexToRgb(config.customPanelBg);
76
189
  document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`);
190
+ document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`);
191
+ document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`);
192
+ document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`);
193
+ document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`);
194
+ document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`);
195
+ document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`);
77
196
  }
78
197
  } else {
79
- document.documentElement.style.removeProperty('--bg-gradient');
80
- document.documentElement.style.removeProperty('--panel-bg');
198
+ [
199
+ '--bg-color',
200
+ '--bg-gradient',
201
+ '--panel-bg',
202
+ '--panel-raised',
203
+ '--panel-soft',
204
+ '--chrome-bg',
205
+ '--surface-bg',
206
+ '--surface-strong',
207
+ '--input-bg'
208
+ ].forEach(name => document.documentElement.style.removeProperty(name));
81
209
  }
82
210
  }
83
211
 
@@ -93,14 +221,19 @@ function hexToRgb(hex) {
93
221
  async function loadTheme() {
94
222
  try {
95
223
  const config = await window.api.getSettings();
224
+ currentSettings = config || {};
96
225
  applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
97
226
  enableVoiceReply = config.enableVoiceReply !== false;
98
227
  ttsProvider = config.ttsProvider || 'google';
99
228
  ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
100
229
  ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
101
230
  ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
231
+ buildProviderPicker(currentSettings);
232
+ syncAgentModeToggle(currentSettings);
102
233
  } catch (e) {
103
234
  applyTheme('dark', '#8b5cf6', '#f8fafc');
235
+ buildProviderPicker(currentSettings);
236
+ syncAgentModeToggle(currentSettings);
104
237
  }
105
238
  }
106
239
 
@@ -116,12 +249,41 @@ function lightenColor(hex, amount) {
116
249
 
117
250
  // 🔔 Real-time theme sync from Settings window
118
251
  window.api.onSettingsChanged((config) => {
252
+ currentSettings = config || currentSettings;
119
253
  applyTheme(config.theme, config.accentColor, config.systemTextColor, config);
120
254
  enableVoiceReply = config.enableVoiceReply !== false;
121
255
  ttsProvider = config.ttsProvider || 'google';
122
256
  ttsVolume = config.ttsVolume !== undefined ? config.ttsVolume : 1.0;
123
257
  ttsSpeed = config.ttsSpeed !== undefined ? config.ttsSpeed : 1.0;
124
258
  ttsPitch = config.ttsPitch !== undefined ? config.ttsPitch : 1.0;
259
+ buildProviderPicker(currentSettings);
260
+ syncAgentModeToggle(currentSettings);
261
+ });
262
+
263
+ chatProviderSelect?.addEventListener('change', (event) => {
264
+ changeChatProvider(event.target.value);
265
+ });
266
+
267
+ agentModeToggle?.addEventListener('change', async () => {
268
+ const nextSettings = {
269
+ ...currentSettings,
270
+ assistantMode: agentModeToggle.checked ? 'agent' : 'chat'
271
+ };
272
+ agentModeToggle.disabled = true;
273
+ try {
274
+ const result = await window.api.saveSettings(nextSettings);
275
+ if (!result || result.success !== false) {
276
+ currentSettings = nextSettings;
277
+ } else {
278
+ throw new Error(result.message || 'Unable to save assistant mode');
279
+ }
280
+ } catch (error) {
281
+ console.error('Failed to change assistant mode:', error);
282
+ setMintActivity('error');
283
+ } finally {
284
+ syncAgentModeToggle(currentSettings);
285
+ agentModeToggle.disabled = false;
286
+ }
125
287
  });
126
288
 
127
289
  // --- Voice Input Setup ---
@@ -190,6 +352,7 @@ function setupSpeechRecognition() {
190
352
  speechRecognition.onstart = () => {
191
353
  micBtn.classList.add('listening');
192
354
  chatInput.placeholder = "Listening... (Click to stop)";
355
+ setMintActivity('listening');
193
356
  speechHadResult = false;
194
357
  if (speechFallbackTimer) clearTimeout(speechFallbackTimer);
195
358
  speechFallbackTimer = setTimeout(() => {
@@ -228,6 +391,7 @@ function setupSpeechRecognition() {
228
391
 
229
392
  speechRecognition.onerror = (err) => {
230
393
  console.error("Speech recognition error:", err);
394
+ setMintActivity('error');
231
395
  fallbackToMediaRecorder();
232
396
  isSpeechStreaming = false;
233
397
  resetMicUI();
@@ -300,10 +464,12 @@ async function setupMediaRecorder() {
300
464
  mediaRecorder.onstart = () => {
301
465
  micBtn.classList.add('listening');
302
466
  chatInput.placeholder = "Listening... (Click to stop)";
467
+ setMintActivity('listening');
303
468
  };
304
469
 
305
470
  } catch (err) {
306
471
  console.error("Microphone access error:", err);
472
+ setMintActivity('error');
307
473
  micBtn.style.display = 'none';
308
474
  appendMessage("❌ ไม่สามารถเข้าถึงไมโครโฟนได้ค่ะ กรุณาตรวจสอบการตั้งค่าระดับระบบ", 'ai');
309
475
  }
@@ -312,11 +478,15 @@ async function setupMediaRecorder() {
312
478
  function resetMicUI() {
313
479
  micBtn.classList.remove('listening');
314
480
  chatInput.placeholder = DEFAULT_PLACEHOLDER;
481
+ if (voiceMode !== 'speech' && (!mediaRecorder || mediaRecorder.state === 'inactive')) {
482
+ setMintActivity('idle');
483
+ }
315
484
  }
316
485
 
317
486
  async function sendVoiceMessage(base64Audio) {
318
487
  showTyping();
319
488
  chatInput.placeholder = "Processing voice...";
489
+ setMintActivity('thinking');
320
490
  try {
321
491
  // Send empty text, but include the audio
322
492
  const response = await window.api.sendMessage("", null, base64Audio);
@@ -330,11 +500,14 @@ async function sendVoiceMessage(base64Audio) {
330
500
  await speakText(normalizeAiText(response.response), { onEnd: resumeSpeechIfNeeded });
331
501
  notifyAiIfNeeded();
332
502
 
333
- if (response.action && response.action.type !== 'none') {
503
+ if (response.approval?.required) {
504
+ appendApprovalCard(msgDiv, response.approval);
505
+ } else if (response.action && response.action.type !== 'none') {
334
506
  appendActionCard(msgDiv, response.action);
335
507
  }
336
508
  } catch (error) {
337
509
  removeTyping();
510
+ setMintActivity('error');
338
511
  appendMessage("ขออภัยค่ะ เกิดข้อผิดพลาดในการประมวลผลเสียง", 'ai');
339
512
  console.error(error);
340
513
  resumeSpeechIfNeeded();
@@ -373,10 +546,10 @@ micBtn.addEventListener('click', (e) => {
373
546
  if (mediaRecorder.state === 'inactive') {
374
547
  audioChunks = [];
375
548
  mediaRecorder.start();
376
- if (window.api && window.api.setAiState) window.api.setAiState('listening');
549
+ setMintActivity('listening');
377
550
  } else {
378
551
  mediaRecorder.stop();
379
- if (window.api && window.api.setAiState) window.api.setAiState('thinking');
552
+ setMintActivity('thinking');
380
553
  voiceMode = null;
381
554
  }
382
555
  return;
@@ -408,10 +581,10 @@ micBtn.addEventListener('click', (e) => {
408
581
  if (mediaRecorder.state === 'inactive') {
409
582
  audioChunks = [];
410
583
  mediaRecorder.start();
411
- if (window.api && window.api.setAiState) window.api.setAiState('listening');
584
+ setMintActivity('listening');
412
585
  } else {
413
586
  mediaRecorder.stop();
414
- if (window.api && window.api.setAiState) window.api.setAiState('thinking');
587
+ setMintActivity('thinking');
415
588
  }
416
589
  });
417
590
 
@@ -419,7 +592,7 @@ micBtn.addEventListener('click', (e) => {
419
592
  let currentAudioPlayer = null;
420
593
 
421
594
  function speakText(text, options = {}) {
422
- if (window.api && window.api.setAiState) window.api.setAiState('speaking');
595
+ setMintActivity('speaking');
423
596
  const onEnd = typeof options.onEnd === 'function' ? options.onEnd : () => {};
424
597
 
425
598
  const wrappedOnEnd = () => {
@@ -429,7 +602,7 @@ function speakText(text, options = {}) {
429
602
 
430
603
  return new Promise(async (resolve) => {
431
604
  if (!enableVoiceReply) {
432
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
605
+ setMintActivity('idle');
433
606
  wrappedOnEnd();
434
607
  return resolve();
435
608
  }
@@ -447,7 +620,7 @@ function speakText(text, options = {}) {
447
620
  }
448
621
 
449
622
  if (!text || !text.trim()) {
450
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
623
+ setMintActivity('idle');
451
624
  wrappedOnEnd();
452
625
  return resolve();
453
626
  }
@@ -461,7 +634,7 @@ function speakText(text, options = {}) {
461
634
  let i = 0;
462
635
  const playNext = () => {
463
636
  if (i >= urls.length) {
464
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
637
+ setMintActivity('idle');
465
638
  wrappedOnEnd();
466
639
  return resolve();
467
640
  }
@@ -499,6 +672,7 @@ function speakText(text, options = {}) {
499
672
 
500
673
  function fallbackSpeak(text, onEnd, resolve) {
501
674
  if (!('speechSynthesis' in window)) {
675
+ setMintActivity('idle');
502
676
  if (onEnd) onEnd();
503
677
  resolve();
504
678
  return;
@@ -515,7 +689,7 @@ function fallbackSpeak(text, onEnd, resolve) {
515
689
  const done = () => {
516
690
  if (finished) return;
517
691
  finished = true;
518
- if (window.api && window.api.setAiState) window.api.setAiState('idle');
692
+ setMintActivity('idle');
519
693
  if (onEnd) onEnd();
520
694
  resolve();
521
695
  };
@@ -540,9 +714,82 @@ maximizeBtn.addEventListener('click', () => {
540
714
  });
541
715
 
542
716
  // Settings button
543
- settingsBtn.addEventListener('click', () => {
717
+ function openSettings() {
544
718
  window.api.openSettings();
545
- });
719
+ }
720
+
721
+ settingsBtn.addEventListener('click', openSettings);
722
+ sidebarSettingsBtn?.addEventListener('click', openSettings);
723
+
724
+ async function renderPicturesLibrary() {
725
+ if (!picturesGrid || !picturesEmpty) return;
726
+ picturesGrid.innerHTML = '';
727
+
728
+ const pictures = await window.api.listSavedPictures();
729
+ picturesEmpty.classList.toggle('is-hidden', pictures.length > 0);
730
+
731
+ for (const picture of pictures) {
732
+ const card = document.createElement('article');
733
+ card.className = 'picture-card';
734
+
735
+ const img = document.createElement('img');
736
+ img.src = picture.url;
737
+ img.alt = picture.filename || 'Saved picture';
738
+ img.loading = 'lazy';
739
+
740
+ const meta = document.createElement('div');
741
+ meta.className = 'picture-card-meta';
742
+ const date = picture.createdAt ? new Date(picture.createdAt).toLocaleString() : '';
743
+ meta.textContent = picture.message || date || picture.filename || 'Saved picture';
744
+ meta.title = [picture.filename, picture.message, date].filter(Boolean).join('\n');
745
+
746
+ card.appendChild(img);
747
+ card.appendChild(meta);
748
+ picturesGrid.appendChild(card);
749
+ }
750
+ }
751
+
752
+ async function openPicturesLibrary() {
753
+ if (!appBody || !picturesLibrary) return;
754
+ picturesLibrary.hidden = false;
755
+ requestAnimationFrame(() => {
756
+ appBody.classList.add('pictures-open');
757
+ });
758
+ sidebarChatBtn?.classList.remove('is-active');
759
+ sidebarPicturesBtn?.classList.add('is-active');
760
+ await renderPicturesLibrary();
761
+ }
762
+
763
+ function closePicturesLibrary() {
764
+ if (!appBody || !picturesLibrary) return;
765
+ appBody.classList.remove('pictures-open');
766
+ setTimeout(() => {
767
+ if (!appBody.classList.contains('pictures-open')) {
768
+ picturesLibrary.hidden = true;
769
+ }
770
+ }, 240);
771
+ sidebarChatBtn?.classList.add('is-active');
772
+ sidebarPicturesBtn?.classList.remove('is-active');
773
+ }
774
+
775
+ sidebarChatBtn?.addEventListener('click', closePicturesLibrary);
776
+ sidebarPicturesBtn?.addEventListener('click', openPicturesLibrary);
777
+ picturesCloseBtn?.addEventListener('click', closePicturesLibrary);
778
+
779
+ function setSidebarCollapsed(isCollapsed) {
780
+ if (!appBody || !sidebarToggleBtn) return;
781
+ appBody.classList.toggle('sidebar-collapsed', isCollapsed);
782
+ sidebarToggleBtn.setAttribute('aria-expanded', String(!isCollapsed));
783
+ sidebarToggleBtn.setAttribute('aria-label', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
784
+ sidebarToggleBtn.setAttribute('title', isCollapsed ? 'Expand sidebar' : 'Collapse sidebar');
785
+ }
786
+
787
+ if (appBody && sidebarToggleBtn) {
788
+ setSidebarCollapsed(true);
789
+ sidebarToggleBtn.addEventListener('click', () => {
790
+ setSidebarCollapsed(!appBody.classList.contains('sidebar-collapsed'));
791
+ });
792
+ }
546
793
 
547
794
  // Throttle utility to prevent UI spam
548
795
  function throttle(func, limit) {
@@ -586,15 +833,141 @@ function formatTime(isoString) {
586
833
  }
587
834
  }
588
835
 
836
+ function compactSmartContext(context) {
837
+ if (!context || typeof context !== 'object') return null;
838
+ const activeWindow = context.activeWindow || {};
839
+ const currentApp = context.currentApp || {};
840
+ const browser = context.browser || null;
841
+ return {
842
+ capturedAt: context.capturedAt,
843
+ platform: context.platform,
844
+ currentApp: currentApp.name || activeWindow.appName || activeWindow.processName || '',
845
+ processName: currentApp.processName || activeWindow.processName || '',
846
+ pid: currentApp.pid || activeWindow.pid || null,
847
+ activeWindowTitle: activeWindow.title || '',
848
+ browser: browser ? {
849
+ title: browser.title || '',
850
+ url: browser.url || '',
851
+ urlUnavailableReason: browser.urlUnavailableReason || ''
852
+ } : null,
853
+ selectedText: context.selectedText || '',
854
+ clipboardText: context.clipboardText || ''
855
+ };
856
+ }
857
+
858
+ function appendSmartContextToMessage(message, context) {
859
+ const compact = compactSmartContext(context);
860
+ if (!compact) return message;
861
+ return [
862
+ message,
863
+ '',
864
+ '[SMART_CONTEXT]',
865
+ 'Use this structured desktop context together with the attached screenshot. Do not mention it unless it helps answer the user.',
866
+ JSON.stringify(compact, null, 2),
867
+ '[/SMART_CONTEXT]'
868
+ ].join('\n');
869
+ }
870
+
871
+ function shouldShowAgentActivity(options = {}) {
872
+ return options.showAgentActivity !== false && currentSettings.assistantMode === 'agent';
873
+ }
874
+
875
+ function createAgentActivityCard() {
876
+ const messageDiv = document.createElement('div');
877
+ messageDiv.classList.add('message', 'ai-message', 'agent-activity-message');
878
+
879
+ const bubble = document.createElement('div');
880
+ bubble.classList.add('message-bubble', 'agent-activity-card');
881
+
882
+ const header = document.createElement('div');
883
+ header.className = 'agent-activity-header';
884
+ const title = document.createElement('span');
885
+ title.textContent = 'Agent Activity';
886
+ const status = document.createElement('span');
887
+ status.className = 'agent-activity-status';
888
+ status.textContent = 'Running';
889
+ header.appendChild(title);
890
+ header.appendChild(status);
891
+
892
+ const list = document.createElement('div');
893
+ list.className = 'agent-activity-list';
894
+
895
+ bubble.appendChild(header);
896
+ bubble.appendChild(list);
897
+ messageDiv.appendChild(bubble);
898
+ chatContainer.appendChild(messageDiv);
899
+ scrollToBottom();
900
+
901
+ return {
902
+ element: messageDiv,
903
+ list,
904
+ status,
905
+ add(label, state = 'running', detail = '') {
906
+ const item = document.createElement('div');
907
+ item.className = 'agent-activity-item';
908
+ item.dataset.state = state;
909
+
910
+ const dot = document.createElement('span');
911
+ dot.className = 'agent-activity-dot';
912
+
913
+ const content = document.createElement('span');
914
+ content.className = 'agent-activity-text';
915
+ content.textContent = detail ? `${label}: ${detail}` : label;
916
+
917
+ item.appendChild(dot);
918
+ item.appendChild(content);
919
+ list.appendChild(item);
920
+ scrollToBottom();
921
+ return item;
922
+ },
923
+ update(item, state, label, detail = '') {
924
+ if (!item) return;
925
+ item.dataset.state = state;
926
+ const content = item.querySelector('.agent-activity-text');
927
+ if (content && label) {
928
+ content.textContent = detail ? `${label}: ${detail}` : label;
929
+ }
930
+ },
931
+ finish(state = 'done', label = 'Done') {
932
+ status.textContent = label;
933
+ status.dataset.state = state;
934
+ }
935
+ };
936
+ }
937
+
938
+ function describeSmartContextActivity(context, hasScreenshot) {
939
+ const compact = compactSmartContext(context) || {};
940
+ const parts = [];
941
+ if (hasScreenshot) parts.push('screen');
942
+ if (compact.currentApp) parts.push(compact.currentApp);
943
+ if (compact.activeWindowTitle) parts.push(compact.activeWindowTitle);
944
+ if (compact.selectedText) parts.push('selected text');
945
+ if (compact.clipboardText) parts.push('clipboard');
946
+ return parts.slice(0, 3).join(' · ') || 'desktop context';
947
+ }
948
+
949
+ function describeActionActivity(action) {
950
+ if (!action || action.type === 'none') return 'No desktop action';
951
+ const meta = getActionCardMeta(action);
952
+ return meta.detail ? `${meta.title} · ${meta.detail}` : meta.title;
953
+ }
954
+
589
955
  // Clear chat history
590
- clearBtn.addEventListener('click', async () => {
956
+ async function clearChatHistory(confirmMessage = 'Clear current chat history?') {
957
+ const shouldClear = window.confirm(confirmMessage);
958
+ if (!shouldClear) return;
959
+
960
+ closePicturesLibrary();
591
961
  await window.api.resetChat();
592
962
  // Remove all messages except the initial greeting
593
963
  const messages = chatContainer.querySelectorAll('.message:not(.initial)');
594
964
  messages.forEach(m => m.remove());
595
965
  // Append a clear confirmation
596
966
  appendMessage('Chat history cleared. Starting fresh! 🌿', 'ai', null, new Date().toISOString());
597
- });
967
+ }
968
+
969
+ clearBtn.addEventListener('click', () => clearChatHistory('Clear current chat history?'));
970
+ sidebarNewChatBtn?.addEventListener('click', () => clearChatHistory('Start a new chat and clear current history?'));
598
971
 
599
972
  function formatProviderInfo(providerInfo) {
600
973
  if (!providerInfo || typeof providerInfo !== 'object') return '';
@@ -604,6 +977,85 @@ function formatProviderInfo(providerInfo) {
604
977
  return model ? `${provider || 'AI'} • ${model}` : provider;
605
978
  }
606
979
 
980
+ function formatNumber(value) {
981
+ const number = Number(value) || 0;
982
+ return number.toLocaleString('en-US');
983
+ }
984
+
985
+ function summarizeProviderUsage(providerInfo) {
986
+ const usage = Array.isArray(providerInfo?.usage) ? providerInfo.usage : [];
987
+ const selectedProvider = String(providerInfo?.provider || '').trim();
988
+ const selectedModel = String(providerInfo?.model || '').trim();
989
+ const row = usage.find(item =>
990
+ String(item.provider || '') === selectedProvider &&
991
+ String(item.model || '') === selectedModel
992
+ ) || usage[0] || {};
993
+
994
+ return {
995
+ requests: Number(row.requests) || 0,
996
+ inputTokens: Number(row.inputTokens) || 0,
997
+ outputTokens: Number(row.outputTokens) || 0,
998
+ reasoningTokens: Number(row.reasoningTokens) || 0,
999
+ cacheReads: Number(row.cacheReads) || 0,
1000
+ totalTokens: Number(row.totalTokens) || 0
1001
+ };
1002
+ }
1003
+
1004
+ function closeProviderPopover() {
1005
+ document.querySelectorAll('.provider-popover').forEach(popover => popover.remove());
1006
+ document.querySelectorAll('.provider-badge.is-open').forEach(badge => badge.classList.remove('is-open'));
1007
+ }
1008
+
1009
+ function createProviderRow(label, value) {
1010
+ const row = document.createElement('div');
1011
+ row.className = 'provider-popover-row';
1012
+ const labelEl = document.createElement('span');
1013
+ labelEl.textContent = label;
1014
+ const valueEl = document.createElement('strong');
1015
+ valueEl.textContent = value;
1016
+ row.appendChild(labelEl);
1017
+ row.appendChild(valueEl);
1018
+ return row;
1019
+ }
1020
+
1021
+ function showProviderPopover(anchor, providerInfo) {
1022
+ closeProviderPopover();
1023
+ anchor.classList.add('is-open');
1024
+
1025
+ const provider = String(providerInfo?.provider || 'AI').trim();
1026
+ const model = String(providerInfo?.model || 'Unknown model').trim();
1027
+ const usage = summarizeProviderUsage(providerInfo);
1028
+ const popover = document.createElement('div');
1029
+ popover.className = 'provider-popover';
1030
+
1031
+ const title = document.createElement('div');
1032
+ title.className = 'provider-popover-title';
1033
+ title.textContent = 'Model details';
1034
+ popover.appendChild(title);
1035
+
1036
+ popover.appendChild(createProviderRow('Provider', provider));
1037
+ popover.appendChild(createProviderRow('Model', model));
1038
+ popover.appendChild(createProviderRow('Context tokens', formatNumber(usage.inputTokens)));
1039
+ popover.appendChild(createProviderRow('Output tokens', formatNumber(usage.outputTokens)));
1040
+ if (usage.reasoningTokens) {
1041
+ popover.appendChild(createProviderRow('Reasoning tokens', formatNumber(usage.reasoningTokens)));
1042
+ }
1043
+ popover.appendChild(createProviderRow('Total tokens', formatNumber(usage.totalTokens)));
1044
+
1045
+ const action = document.createElement('button');
1046
+ action.type = 'button';
1047
+ action.className = 'provider-popover-action';
1048
+ action.textContent = 'Change model in Settings';
1049
+ action.addEventListener('click', (event) => {
1050
+ event.stopPropagation();
1051
+ closeProviderPopover();
1052
+ if (window.api?.openSettings) window.api.openSettings();
1053
+ });
1054
+ popover.appendChild(action);
1055
+
1056
+ anchor.after(popover);
1057
+ }
1058
+
607
1059
  function splitListOutro(text) {
608
1060
  const value = String(text || '').trim();
609
1061
  const markers = [
@@ -726,10 +1178,20 @@ function appendMessage(text, sender, base64Image = null, timestamp = null, optio
726
1178
  const timeDiv = document.createElement('div');
727
1179
  timeDiv.classList.add('message-time');
728
1180
  if (providerLabel) {
729
- const providerSpan = document.createElement('span');
730
- providerSpan.classList.add('provider-badge');
731
- providerSpan.textContent = providerLabel;
732
- timeDiv.appendChild(providerSpan);
1181
+ const providerButton = document.createElement('button');
1182
+ providerButton.type = 'button';
1183
+ providerButton.classList.add('provider-badge');
1184
+ providerButton.textContent = providerLabel;
1185
+ providerButton.title = 'View model details';
1186
+ providerButton.addEventListener('click', (event) => {
1187
+ event.stopPropagation();
1188
+ if (providerButton.classList.contains('is-open')) {
1189
+ closeProviderPopover();
1190
+ return;
1191
+ }
1192
+ showProviderPopover(providerButton, options.providerInfo);
1193
+ });
1194
+ timeDiv.appendChild(providerButton);
733
1195
  }
734
1196
  if (timestamp) {
735
1197
  const timeSpan = document.createElement('span');
@@ -834,29 +1296,193 @@ function autoChunkAiText(text) {
834
1296
  }
835
1297
 
836
1298
  function appendActionCard(messageDiv, action) {
1299
+ if (!messageDiv || !action || action.type === 'none') return;
1300
+
1301
+ const meta = getActionCardMeta(action);
837
1302
  const card = document.createElement('div');
838
1303
  card.classList.add('action-card');
1304
+ card.dataset.actionType = action.type || 'unknown';
839
1305
 
840
- let icon = '';
841
- let text = '';
842
-
843
- if (action.type === 'open_url') {
844
- icon = '🌐';
845
- text = `Opened URL: ${action.target}`;
846
- } else if (action.type === 'open_app') {
847
- icon = '🚀';
848
- text = `Launched App: ${action.target}`;
849
- } else if (action.type === 'search') {
850
- icon = '🔍';
851
- text = `Searched info: ${action.target}`;
852
- } else {
853
- return; // Do nothing if none or unknown
1306
+ const icon = document.createElement('span');
1307
+ icon.className = 'action-card-icon';
1308
+ icon.textContent = meta.icon;
1309
+
1310
+ const content = document.createElement('div');
1311
+ content.className = 'action-card-content';
1312
+
1313
+ const title = document.createElement('div');
1314
+ title.className = 'action-card-title';
1315
+ title.textContent = meta.title;
1316
+ content.appendChild(title);
1317
+
1318
+ if (meta.detail) {
1319
+ const detail = document.createElement('div');
1320
+ detail.className = 'action-card-detail';
1321
+ detail.textContent = meta.detail;
1322
+ content.appendChild(detail);
854
1323
  }
855
1324
 
856
- card.textContent = `${icon} ${text}`;
1325
+ card.appendChild(icon);
1326
+ card.appendChild(content);
1327
+ messageDiv.querySelector('.message-bubble')?.appendChild(card);
1328
+ }
1329
+
1330
+ function getActionCardMeta(action) {
1331
+ const target = formatActionTarget(action);
1332
+ const type = action?.type || 'unknown';
1333
+ const targetOrFallback = target || 'No target';
1334
+
1335
+ const map = {
1336
+ open_url: ['🌐', 'Opened URL', target],
1337
+ search: ['🔍', 'Searched the web', target],
1338
+ open_app: ['🚀', 'Launched app', target],
1339
+ web_automation: ['🧭', 'Ran browser automation', target],
1340
+ create_folder: ['📁', 'Created folder', target],
1341
+ open_file: ['📄', 'Opened file', target],
1342
+ open_folder: ['📂', 'Opened folder', target],
1343
+ delete_file: ['🗑️', 'Deleted file', target],
1344
+ find_path: ['🔎', action.openAfter ? 'Found and opened path' : 'Found path', buildFindPathDetail(action)],
1345
+ clipboard_write: ['📋', 'Updated clipboard', target],
1346
+ learn_file: ['📚', 'Indexed file', target],
1347
+ learn_folder: ['📚', 'Indexed folder', target],
1348
+ system_info: ['💻', target ? 'Checked weather' : 'Checked system info', target],
1349
+ plugin: ['🔌', 'Ran plugin', target],
1350
+ mcp_tool: ['🧩', 'Called MCP tool', target],
1351
+ mouse_move: ['↗', 'Moved pointer', target],
1352
+ mouse_click: ['☝', 'Clicked screen', buildMouseDetail(action)],
1353
+ type_text: ['⌨', 'Typed text', target],
1354
+ key_tap: ['⌨', 'Pressed key', target],
1355
+ system_automation: ['⚙', 'Changed system setting', target]
1356
+ };
1357
+
1358
+ const [icon, title, detail] = map[type] || ['⚡', `Ran action: ${type}`, targetOrFallback];
1359
+ return { icon, title, detail };
1360
+ }
1361
+
1362
+ function buildFindPathDetail(action) {
1363
+ const target = formatActionTarget(action);
1364
+ const typeLabel = action.pathType && action.pathType !== 'any' ? ` (${action.pathType})` : '';
1365
+ return target ? `${target}${typeLabel}` : typeLabel.trim();
1366
+ }
1367
+
1368
+ function buildMouseDetail(action) {
1369
+ const point = formatActionTarget(action);
1370
+ const button = action.button ? `button ${action.button}` : 'left button';
1371
+ return point ? `${point} · ${button}` : button;
1372
+ }
1373
+
1374
+ function formatActionTarget(action) {
1375
+ if (!action || typeof action !== 'object') return '';
1376
+ if (action.server && action.target) return `${action.server}:${action.target}`;
1377
+ if (action.pluginName) return `${action.pluginName} ${action.target || ''}`.trim();
1378
+ if (action.target) return String(action.target);
1379
+ if (Number.isFinite(action.x) && Number.isFinite(action.y)) return `${action.x}, ${action.y}`;
1380
+ return '';
1381
+ }
1382
+
1383
+ function getApprovalCopy(approval) {
1384
+ const action = approval?.action || {};
1385
+ const actionType = action.type || 'unknown';
1386
+ const target = formatActionTarget(action);
1387
+ const isDangerous = approval?.tier === 'dangerous';
1388
+ return {
1389
+ title: isDangerous ? 'Dangerous action requires approval' : 'Action requires approval',
1390
+ body: target ? `${actionType}: ${target}` : actionType,
1391
+ reason: approval?.reason || 'This action needs your permission before Mint can run it.',
1392
+ approveLabel: isDangerous ? 'Allow Dangerous Action' : 'Allow Action'
1393
+ };
1394
+ }
1395
+
1396
+ function appendApprovalCard(messageDiv, approval, activity = null) {
1397
+ if (!messageDiv || !approval?.action || !window.api?.executeApprovedAction) return;
1398
+
1399
+ const copy = getApprovalCopy(approval);
1400
+ const card = document.createElement('div');
1401
+ card.classList.add('action-card', 'approval-card');
1402
+ card.dataset.tier = approval.tier || 'approval';
1403
+
1404
+ const content = document.createElement('div');
1405
+ content.className = 'approval-card-content';
1406
+
1407
+ const title = document.createElement('div');
1408
+ title.className = 'approval-card-title';
1409
+ title.textContent = copy.title;
1410
+
1411
+ const body = document.createElement('div');
1412
+ body.className = 'approval-card-body';
1413
+ body.textContent = copy.body;
1414
+
1415
+ const reason = document.createElement('div');
1416
+ reason.className = 'approval-card-reason';
1417
+ reason.textContent = copy.reason;
1418
+
1419
+ content.appendChild(title);
1420
+ content.appendChild(body);
1421
+ content.appendChild(reason);
1422
+
1423
+ const actions = document.createElement('div');
1424
+ actions.className = 'approval-card-actions';
1425
+
1426
+ const approveBtn = document.createElement('button');
1427
+ approveBtn.type = 'button';
1428
+ approveBtn.className = 'approval-btn approval-btn-approve';
1429
+ approveBtn.textContent = copy.approveLabel;
1430
+
1431
+ const cancelBtn = document.createElement('button');
1432
+ cancelBtn.type = 'button';
1433
+ cancelBtn.className = 'approval-btn approval-btn-cancel';
1434
+ cancelBtn.textContent = 'Cancel';
1435
+
1436
+ const setDone = (message, state) => {
1437
+ approveBtn.disabled = true;
1438
+ cancelBtn.disabled = true;
1439
+ card.dataset.state = state;
1440
+ reason.textContent = message;
1441
+ };
1442
+
1443
+ approveBtn.addEventListener('click', async () => {
1444
+ approveBtn.disabled = true;
1445
+ cancelBtn.disabled = true;
1446
+ reason.textContent = 'Running approved action...';
1447
+ const runStep = activity?.add('Running approved action', 'running', describeActionActivity(approval.action));
1448
+ setMintActivity('thinking');
1449
+
1450
+ try {
1451
+ const result = await window.api.executeApprovedAction(approval.action);
1452
+ if (!result || result.success === false) {
1453
+ setDone(result?.message || 'Action failed.', 'error');
1454
+ activity?.update(runStep, 'error', 'Action failed', result?.message || '');
1455
+ activity?.finish('error', 'Failed');
1456
+ setMintActivity('error');
1457
+ return;
1458
+ }
1459
+
1460
+ setDone(result.message || 'Action completed.', 'approved');
1461
+ activity?.update(runStep, 'done', 'Action completed', result.message || describeActionActivity(approval.action));
1462
+ activity?.finish('done', 'Completed');
1463
+ setMintActivity('idle');
1464
+ } catch (error) {
1465
+ console.error('[Approval] Failed to execute action:', error);
1466
+ setDone(error.message || 'Action failed.', 'error');
1467
+ activity?.update(runStep, 'error', 'Action failed', error.message || '');
1468
+ activity?.finish('error', 'Failed');
1469
+ setMintActivity('error');
1470
+ }
1471
+ });
1472
+
1473
+ cancelBtn.addEventListener('click', () => {
1474
+ setDone('Cancelled by user.', 'cancelled');
1475
+ activity?.add('Approval cancelled', 'cancelled');
1476
+ activity?.finish('cancelled', 'Cancelled');
1477
+ setMintActivity('idle');
1478
+ });
1479
+
1480
+ actions.appendChild(approveBtn);
1481
+ actions.appendChild(cancelBtn);
1482
+ card.appendChild(content);
1483
+ card.appendChild(actions);
857
1484
 
858
- // Append after the bubble
859
- messageDiv.querySelector('.message-bubble').appendChild(card);
1485
+ messageDiv.querySelector('.message-bubble')?.appendChild(card);
860
1486
  }
861
1487
 
862
1488
  function showTyping() {
@@ -898,8 +1524,18 @@ function loadScript(src) {
898
1524
  });
899
1525
  }
900
1526
 
1527
+ function hideStartupLoading() {
1528
+ appContainer?.classList.remove('is-loading');
1529
+ if (!startupLoading) return;
1530
+ startupLoading.classList.add('is-hidden');
1531
+ setTimeout(() => startupLoading.remove(), 400);
1532
+ }
1533
+
901
1534
  async function loadLive2DWhenIdle() {
902
- if (!modelMount || window.Live2DManager) return;
1535
+ if (!modelMount || window.Live2DManager) {
1536
+ hideStartupLoading();
1537
+ return;
1538
+ }
903
1539
  try {
904
1540
  await loadScript('../../node_modules/@hazart-pkg/live2d-core/live2dcubismcore.min.js');
905
1541
  await loadScript('../../node_modules/pixi.js/dist/browser/pixi.min.js');
@@ -907,6 +1543,7 @@ async function loadLive2DWhenIdle() {
907
1543
  await loadScript('live2d_manager.js');
908
1544
  if (window.Live2DManager) {
909
1545
  await Live2DManager.loadModel(modelMount, modelStatus, modelShell);
1546
+ applyModelPanelControlState();
910
1547
  }
911
1548
  } catch (err) {
912
1549
  console.error('[Live2D] Deferred load failed:', err);
@@ -914,6 +1551,8 @@ async function loadLive2DWhenIdle() {
914
1551
  modelStatus.classList.add('is-error');
915
1552
  modelStatus.textContent = 'Live2D model unavailable.';
916
1553
  }
1554
+ } finally {
1555
+ hideStartupLoading();
917
1556
  }
918
1557
  }
919
1558
 
@@ -978,30 +1617,55 @@ async function sendTextMessage(text, options = {}) {
978
1617
  rememberConversationLanguage(displayText || cleanText);
979
1618
  }
980
1619
 
1620
+ const activity = shouldShowAgentActivity(options) ? createAgentActivityCard() : null;
1621
+ const contextStep = activity?.add('Preparing desktop context', 'running');
1622
+
981
1623
  // Show typing early so user knows we are processing
982
1624
  showTyping();
1625
+ setMintActivity('thinking');
1626
+
1627
+ let messageToSend = cleanText;
983
1628
 
984
1629
  // Check Smart Context Toggle
985
1630
  const smartToggle = document.getElementById('smart-context-toggle');
986
1631
  if (allowSmartContext && smartToggle && smartToggle.checked && !imageToSend) {
987
1632
  try {
988
- const silentCapture = await window.api.captureSilentScreen();
1633
+ const [silentCapture, smartContext] = await Promise.all([
1634
+ window.api.captureSilentScreen(),
1635
+ window.api.getSmartContext ? window.api.getSmartContext() : Promise.resolve(null)
1636
+ ]);
989
1637
  if (silentCapture) {
990
1638
  // Set imageToSend so it gets sent to the API, but we already appended the chat bubble
991
1639
  imageToSend = silentCapture;
992
1640
  }
1641
+ if (smartContext) {
1642
+ messageToSend = appendSmartContextToMessage(cleanText, smartContext);
1643
+ }
1644
+ if (activity && contextStep) {
1645
+ activity.update(
1646
+ contextStep,
1647
+ 'done',
1648
+ 'Read Smart Context',
1649
+ describeSmartContextActivity(smartContext, Boolean(silentCapture))
1650
+ );
1651
+ }
993
1652
  } catch (err) {
994
1653
  console.error("Smart Context capture failed:", err);
1654
+ activity?.update(contextStep, 'error', 'Smart Context unavailable', err.message || '');
995
1655
  }
1656
+ } else if (activity && contextStep) {
1657
+ activity.update(contextStep, 'skipped', 'Smart Context skipped', imageToSend ? 'image already attached' : 'toggle is off');
996
1658
  }
997
1659
 
998
1660
  // Hide proactive bar if user is actively typing a message
999
1661
  hideProactiveBar();
1662
+ const modelStep = activity?.add('Waiting for model response', 'running');
1000
1663
 
1001
1664
  try {
1002
1665
  // Send to main process (text, image, audio=null)
1003
- const response = await window.api.sendMessage(cleanText, imageToSend, null);
1666
+ const response = await window.api.sendMessage(messageToSend, imageToSend, null);
1004
1667
  removeTyping();
1668
+ activity?.update(modelStep, 'done', 'Model response received');
1005
1669
 
1006
1670
  if (typeof response.response !== 'string') {
1007
1671
  response.response = normalizeAiText(response.response);
@@ -1009,6 +1673,7 @@ async function sendTextMessage(text, options = {}) {
1009
1673
 
1010
1674
  // Handle system_info action: fetch data and append to AI message
1011
1675
  if (response.action && response.action.type === 'system_info') {
1676
+ const infoStep = activity?.add('Running local info action', 'running', describeActionActivity(response.action));
1012
1677
  const city = (response.action.target || '').trim();
1013
1678
  // Only treat as weather if city looks like a real location name (not blank, not 'date', not 'time')
1014
1679
  const weatherKeywords = ['date', 'time', 'วัน', 'เวลา', 'today', 'now'];
@@ -1018,12 +1683,14 @@ async function sendTextMessage(text, options = {}) {
1018
1683
  // Weather query
1019
1684
  const weather = await window.api.getWeather(city);
1020
1685
  response.response += `\n\n🌡️ ${weather.data}`;
1686
+ activity?.update(infoStep, 'done', 'Weather info added', city);
1021
1687
  } else {
1022
1688
  // General system info (date, time, RAM, CPU)
1023
1689
  const info = await window.api.getSystemInfo();
1024
1690
  const machine = info.machine && info.machine.display ? `\n🖥️ รุ่นเครื่อง: ${info.machine.display}` : '';
1025
1691
  const distro = info.distro ? `\nระบบ: ${info.distro}` : '';
1026
1692
  response.response += `\n\n📅 วันนี้: ${info.date}\n⏰ เวลา: ${info.time}${machine}${distro}\n💻 CPU: ${info.cpu.model} (${info.cpu.cores} คอร์)\n💻 RAM: ${info.ram.used} / ${info.ram.total} (${info.ram.percent})`;
1693
+ activity?.update(infoStep, 'done', 'System info added');
1027
1694
  }
1028
1695
  }
1029
1696
 
@@ -1039,11 +1706,27 @@ async function sendTextMessage(text, options = {}) {
1039
1706
  notifyAiIfNeeded();
1040
1707
 
1041
1708
  // Append action card if applicable
1042
- if (response.action && response.action.type !== 'none' && response.action.type !== 'system_info') {
1709
+ if (response.approval?.required) {
1710
+ activity?.add('Selected action', 'approval', describeActionActivity(response.approval.action));
1711
+ activity?.add('Waiting for approval', 'running', response.approval.reason || '');
1712
+ activity?.finish('waiting', 'Waiting');
1713
+ appendApprovalCard(msgDiv, response.approval, activity);
1714
+ } else if (response.action && response.action.type !== 'none' && response.action.type !== 'system_info') {
1715
+ activity?.add('Selected action', 'done', describeActionActivity(response.action));
1043
1716
  appendActionCard(msgDiv, response.action);
1717
+ activity?.finish('done', 'Completed');
1718
+ } else if (response.action && response.action.type === 'system_info') {
1719
+ activity?.add('Selected action', 'done', describeActionActivity(response.action));
1720
+ activity?.finish('done', 'Completed');
1721
+ } else {
1722
+ activity?.add('No desktop action selected', 'done');
1723
+ activity?.finish('done', 'Completed');
1044
1724
  }
1045
1725
  } catch (error) {
1046
1726
  removeTyping();
1727
+ setMintActivity('error');
1728
+ activity?.update(modelStep, 'error', 'Model request failed', error.message || '');
1729
+ activity?.finish('error', 'Failed');
1047
1730
  appendMessage("Sorry, I encountered an error communicating with the main process.", 'ai');
1048
1731
  console.error(error);
1049
1732
  resumeSpeechIfNeeded();
@@ -1052,7 +1735,6 @@ async function sendTextMessage(text, options = {}) {
1052
1735
 
1053
1736
  chatForm.addEventListener('submit', throttle(async (e) => {
1054
1737
  e.preventDefault();
1055
- if (window.api && window.api.setAiState) window.api.setAiState('thinking');
1056
1738
  const text = chatInput.value.trim();
1057
1739
  await sendTextMessage(text);
1058
1740
  }, 500));
@@ -1060,7 +1742,7 @@ chatForm.addEventListener('submit', throttle(async (e) => {
1060
1742
  window.addEventListener('live2d-model-interaction', async (event) => {
1061
1743
  const prompt = event?.detail?.prompt;
1062
1744
  if (!prompt) return;
1063
- if (window.api && window.api.setAiState) window.api.setAiState('thinking');
1745
+ setMintActivity('thinking');
1064
1746
  const interactionPrompt = `${prompt}\n\n${buildInteractionLanguageInstruction()}`;
1065
1747
  const displayPrefix = lastConversationLanguage === 'thai' ? 'แตะโมเดล' : 'Model interaction';
1066
1748
  await sendTextMessage(interactionPrompt, {
@@ -1126,9 +1808,9 @@ inputArea.addEventListener('drop', (e) => {
1126
1808
  window.addEventListener('DOMContentLoaded', async () => {
1127
1809
  chatInput.focus();
1128
1810
  await loadTheme();
1811
+ setMintActivity('idle');
1129
1812
  await loadChatHistory();
1130
- const scheduleLive2DLoad = window.requestIdleCallback || ((callback) => setTimeout(callback, 750));
1131
- scheduleLive2DLoad(() => loadLive2DWhenIdle());
1813
+ loadLive2DWhenIdle();
1132
1814
  });
1133
1815
 
1134
1816
  // Proactive OS Notifications (Battery, Network, etc.)
@@ -1143,6 +1825,11 @@ window.addEventListener('focus', () => {
1143
1825
  if (window.api.clearAiNotifications) window.api.clearAiNotifications();
1144
1826
  });
1145
1827
 
1828
+ document.addEventListener('click', closeProviderPopover);
1829
+ document.addEventListener('keydown', (event) => {
1830
+ if (event.key === 'Escape') closeProviderPopover();
1831
+ });
1832
+
1146
1833
  // =====================
1147
1834
  // Proactive Smart Suggestion Engine
1148
1835
  // =====================
@@ -1224,34 +1911,121 @@ if (smartContextToggle) {
1224
1911
  // Toggle Live2D Model visibility
1225
1912
  const toggleModelBtn = document.getElementById('toggle-model-btn');
1226
1913
  const assistantWorkspace = document.querySelector('.assistant-workspace');
1914
+ const modelLockBtn = document.getElementById('model-lock-btn');
1915
+ const modelScaleSlider = document.getElementById('model-scale-slider');
1916
+ const modelScaleValue = document.getElementById('model-scale-value');
1917
+ const modelScaleResetBtn = document.getElementById('model-scale-reset-btn');
1918
+ const modelBgBtn = document.getElementById('model-bg-btn');
1919
+ const layoutPresetBtns = document.querySelectorAll('.layout-preset-btn');
1920
+
1921
+ const modelBgStorageKey = 'mint-model-background';
1922
+ const modelScaleStorageKey = 'mint-model-scale';
1923
+ const modelPositionLockStorageKey = 'mint-model-position-locked';
1924
+ const workspaceLayoutStorageKey = 'mint-workspace-layout';
1925
+ const modelBgClasses = ['model-bg-default', 'model-bg-clear', 'model-bg-grid', 'model-bg-stage'];
1926
+ const modelBgLabels = ['Default background', 'Clear background', 'Grid background', 'Stage background'];
1927
+ const workspaceLayoutClasses = ['layout-chat'];
1928
+ const workspaceLayoutPresets = ['companion', 'chat'];
1929
+
1930
+ function setModelHidden(isHidden) {
1931
+ if (!assistantWorkspace || !toggleModelBtn) return;
1932
+ assistantWorkspace.classList.toggle('model-hidden', Boolean(isHidden));
1933
+ toggleModelBtn.classList.toggle('active', Boolean(isHidden));
1934
+ toggleModelBtn.setAttribute('aria-pressed', String(Boolean(isHidden)));
1935
+ localStorage.setItem('mint-model-hidden', String(Boolean(isHidden)));
1936
+
1937
+ if (!isHidden && window.Live2DManager && Live2DManager.model) {
1938
+ setTimeout(() => {
1939
+ window.dispatchEvent(new Event('resize'));
1940
+ if (typeof Live2DManager.fitModelToMount === 'function') {
1941
+ Live2DManager.fitModelToMount();
1942
+ }
1943
+ }, 450);
1944
+ }
1945
+ }
1946
+
1947
+ function setModelPositionLocked(isLocked) {
1948
+ const locked = Boolean(isLocked);
1949
+ localStorage.setItem(modelPositionLockStorageKey, String(locked));
1950
+ modelLockBtn?.classList.toggle('is-active', locked);
1951
+ modelLockBtn?.setAttribute('aria-pressed', String(locked));
1952
+ modelLockBtn?.setAttribute('title', locked ? 'Unlock model position' : 'Lock model position');
1953
+ if (window.Live2DManager) {
1954
+ Live2DManager.setPointerTrackingEnabled(!locked);
1955
+ }
1956
+ }
1957
+
1958
+ function setModelBackground(index) {
1959
+ if (!modelShell) return;
1960
+ const normalized = ((Number(index) || 0) + modelBgClasses.length) % modelBgClasses.length;
1961
+ modelBgClasses.forEach(className => modelShell.classList.remove(className));
1962
+ if (normalized > 0) {
1963
+ modelShell.classList.add(modelBgClasses[normalized]);
1964
+ }
1965
+ localStorage.setItem(modelBgStorageKey, String(normalized));
1966
+ modelBgBtn?.setAttribute('title', modelBgLabels[normalized]);
1967
+ }
1968
+
1969
+ function setModelScale(value) {
1970
+ const next = Math.max(78, Math.min(128, Number(value) || 100));
1971
+ localStorage.setItem(modelScaleStorageKey, String(next));
1972
+ if (modelScaleSlider) modelScaleSlider.value = String(next);
1973
+ if (modelScaleValue) modelScaleValue.textContent = `${(next / 100).toFixed(2)}x`;
1974
+ if (window.Live2DManager) {
1975
+ Live2DManager.setZoomMultiplier(next / 100);
1976
+ }
1977
+ }
1978
+
1979
+ function applyModelPanelControlState() {
1980
+ setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) === 'true');
1981
+ setModelBackground(Number(localStorage.getItem(modelBgStorageKey) || 0));
1982
+ setModelScale(Number(localStorage.getItem(modelScaleStorageKey) || 100));
1983
+ setWorkspaceLayout(localStorage.getItem(workspaceLayoutStorageKey) || 'companion');
1984
+ }
1985
+
1986
+ function setWorkspaceLayout(layout) {
1987
+ if (!assistantWorkspace) return;
1988
+ const normalized = workspaceLayoutPresets.includes(layout) ? layout : 'companion';
1989
+ workspaceLayoutClasses.forEach(className => assistantWorkspace.classList.remove(className));
1990
+ if (normalized !== 'companion') {
1991
+ assistantWorkspace.classList.add(`layout-${normalized}`);
1992
+ }
1993
+ localStorage.setItem(workspaceLayoutStorageKey, normalized);
1994
+ layoutPresetBtns.forEach((button) => {
1995
+ const isActive = button.dataset.layoutPreset === normalized;
1996
+ button.classList.toggle('is-active', isActive);
1997
+ button.setAttribute('aria-pressed', String(isActive));
1998
+ });
1999
+ }
1227
2000
 
1228
2001
  if (toggleModelBtn && assistantWorkspace) {
1229
2002
  toggleModelBtn.addEventListener('click', () => {
1230
- const isHidden = assistantWorkspace.classList.toggle('model-hidden');
1231
- toggleModelBtn.classList.toggle('active', isHidden);
1232
-
1233
- // Save preference to local storage
1234
- localStorage.setItem('mint-model-hidden', String(isHidden));
1235
-
1236
- // Refit model if shown
1237
- if (!isHidden && window.Live2DManager && Live2DManager.model) {
1238
- // Wait for transition
1239
- setTimeout(() => {
1240
- // Trigger a resize event to refit
1241
- window.dispatchEvent(new Event('resize'));
1242
- }, 450);
1243
- }
2003
+ setModelHidden(!assistantWorkspace.classList.contains('model-hidden'));
1244
2004
  });
1245
2005
 
1246
2006
  // Restore preference on load
1247
2007
  const savedModelHidden = localStorage.getItem('mint-model-hidden');
1248
2008
  const savedHidden = savedModelHidden === null || savedModelHidden === 'true';
1249
2009
  if (savedHidden) {
1250
- assistantWorkspace.classList.add('model-hidden');
1251
- toggleModelBtn.classList.add('active');
2010
+ setModelHidden(true);
1252
2011
  }
1253
2012
  }
1254
2013
 
2014
+ modelLockBtn?.addEventListener('click', () => {
2015
+ setModelPositionLocked(localStorage.getItem(modelPositionLockStorageKey) !== 'true');
2016
+ });
2017
+ modelScaleSlider?.addEventListener('input', (event) => setModelScale(event.target.value));
2018
+ modelScaleResetBtn?.addEventListener('click', () => setModelScale(100));
2019
+ modelBgBtn?.addEventListener('click', () => {
2020
+ const current = Number(localStorage.getItem(modelBgStorageKey) || 0);
2021
+ setModelBackground(current + 1);
2022
+ });
2023
+ layoutPresetBtns.forEach((button) => {
2024
+ button.addEventListener('click', () => setWorkspaceLayout(button.dataset.layoutPreset));
2025
+ });
2026
+
2027
+ applyModelPanelControlState();
2028
+
1255
2029
  // Cycle Shiroko's Expression
1256
2030
  const changeExpressionBtn = document.getElementById('change-expression-btn');
1257
2031
  if (changeExpressionBtn) {