@pheem49/mint 1.5.0 → 1.5.2

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 (101) hide show
  1. package/README.md +35 -1
  2. package/main.js +28 -14
  3. package/mint-cli-logic.js +3 -119
  4. package/mint-cli.js +201 -500
  5. package/models/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  6. package/models/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +14 -0
  7. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json +40 -0
  8. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json +15 -0
  9. package/models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json +10 -0
  10. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json +50 -0
  11. package/models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json +10 -0
  12. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json +15 -0
  13. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json +10 -0
  14. package/models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json +10 -0
  15. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png +0 -0
  16. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png +0 -0
  17. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png +0 -0
  18. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png +0 -0
  19. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json +1498 -0
  20. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3 +0 -0
  21. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +47 -0
  22. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json +6658 -0
  23. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json +1299 -0
  24. package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +23 -0
  25. package/package.json +40 -17
  26. package/src/AI_Brain/Gemini_API.js +147 -46
  27. package/src/AI_Brain/autonomous_brain.js +2 -1
  28. package/src/AI_Brain/memory_store.js +299 -3
  29. package/src/AI_Brain/proactive_engine.js +12 -2
  30. package/src/Automation_Layer/browser_automation.js +26 -24
  31. package/src/CLI/approval_handler.js +42 -0
  32. package/src/CLI/chat_router.js +18 -6
  33. package/src/CLI/chat_ui.js +583 -52
  34. package/src/CLI/cli_colors.js +32 -0
  35. package/src/CLI/cli_formatters.js +89 -0
  36. package/src/CLI/code_agent.js +369 -71
  37. package/src/CLI/image_input.js +90 -0
  38. package/src/CLI/intent_detectors.js +181 -0
  39. package/src/CLI/interactive_chat.js +479 -0
  40. package/src/CLI/list_features.js +3 -0
  41. package/src/CLI/onboarding.js +72 -15
  42. package/src/CLI/repo_summarizer.js +282 -0
  43. package/src/CLI/semantic_code_search.js +312 -0
  44. package/src/CLI/skill_manager.js +41 -0
  45. package/src/CLI/slash_command_handler.js +418 -0
  46. package/src/CLI/symbol_indexer.js +231 -0
  47. package/src/CLI/updater.js +6 -4
  48. package/src/Channels/discord_bridge.js +11 -13
  49. package/src/Channels/line_bridge.js +10 -10
  50. package/src/Channels/slack_bridge.js +7 -12
  51. package/src/Channels/telegram_bridge.js +6 -14
  52. package/src/Channels/whatsapp_bridge.js +11 -9
  53. package/src/System/action_executor.js +59 -10
  54. package/src/System/chat_history_manager.js +20 -12
  55. package/src/System/config_manager.js +31 -1
  56. package/src/System/granular_automation.js +122 -53
  57. package/src/System/optional_require.js +23 -0
  58. package/src/System/proactive_loop.js +19 -3
  59. package/src/System/safety_manager.js +108 -0
  60. package/src/System/sandbox_runner.js +182 -0
  61. package/src/System/system_automation.js +127 -81
  62. package/src/System/system_info.js +70 -0
  63. package/src/System/tool_registry.js +280 -0
  64. package/src/System/window_manager.js +4 -2
  65. package/src/UI/live2d_manager.js +566 -0
  66. package/src/UI/renderer.js +339 -21
  67. package/src/UI/settings.css +655 -420
  68. package/src/UI/settings.html +478 -432
  69. package/src/UI/settings.js +10 -8
  70. package/src/UI/styles.css +516 -31
  71. package/.codex +0 -0
  72. package/docs/assets/Agent_Mint.png +0 -0
  73. package/docs/assets/CLI_Screen.png +0 -0
  74. package/docs/assets/Settings.png +0 -0
  75. package/docs/assets/icon.png +0 -0
  76. package/docs/guide.html +0 -632
  77. package/docs/index.html +0 -133
  78. package/docs/style.css +0 -579
  79. package/index.html +0 -16
  80. package/src/UI/index.html +0 -126
  81. package/tech_news.txt +0 -3
  82. package/test_knowledge.txt +0 -3
  83. package/tests/action_executor_safety.test.js +0 -67
  84. package/tests/agent_orchestrator.test.js +0 -41
  85. package/tests/chat_router.test.js +0 -42
  86. package/tests/code_agent.test.js +0 -69
  87. package/tests/config_manager.test.js +0 -141
  88. package/tests/docker.test.js +0 -46
  89. package/tests/file_operations.test.js +0 -57
  90. package/tests/gmail.test.js +0 -135
  91. package/tests/gmail_auth.test.js +0 -129
  92. package/tests/google_calendar.test.js +0 -113
  93. package/tests/google_tts_urls.test.js +0 -24
  94. package/tests/memory_store.test.js +0 -185
  95. package/tests/notion.test.js +0 -121
  96. package/tests/provider_routing.test.js +0 -83
  97. package/tests/safety_manager.test.js +0 -40
  98. package/tests/spotify.test.js +0 -201
  99. package/tests/system_monitor.test.js +0 -37
  100. package/tests/updater.test.js +0 -32
  101. package/tests/workspace_manager.test.js +0 -56
@@ -11,6 +11,9 @@ const visionBtn = document.getElementById('vision-btn');
11
11
  const imagePreviewContainer = document.getElementById('image-preview-container');
12
12
  const imagePreview = document.getElementById('image-preview');
13
13
  const removeImageBtn = document.getElementById('remove-image-btn');
14
+ const modelMount = document.getElementById('model-mount');
15
+ const modelShell = document.getElementById('model-shell');
16
+ const modelStatus = document.getElementById('model-status');
14
17
 
15
18
  // Proactive Assistant elements
16
19
  const proactiveBar = document.getElementById('proactive-bar');
@@ -24,6 +27,31 @@ let ttsProvider = 'google';
24
27
  let ttsVolume = 1.0;
25
28
  let ttsSpeed = 1.0;
26
29
  let ttsPitch = 1.0;
30
+ let lastConversationLanguage = 'auto';
31
+
32
+ function detectConversationLanguage(text) {
33
+ const value = String(text || '');
34
+ if (/[\u0E00-\u0E7F]/.test(value)) return 'thai';
35
+ if (/[A-Za-z]/.test(value)) return 'english';
36
+ return 'auto';
37
+ }
38
+
39
+ function rememberConversationLanguage(text) {
40
+ const detected = detectConversationLanguage(text);
41
+ if (detected !== 'auto') {
42
+ lastConversationLanguage = detected;
43
+ }
44
+ }
45
+
46
+ function buildInteractionLanguageInstruction() {
47
+ if (lastConversationLanguage === 'thai') {
48
+ return 'Current conversation language: Thai. Reply in Thai. Do not reply in English just because this interaction instruction is written in English.';
49
+ }
50
+ if (lastConversationLanguage === 'english') {
51
+ return 'Current conversation language: English. Reply in English. Do not switch to Thai.';
52
+ }
53
+ return 'Infer the reply language from the recent conversation before this interaction instruction, not from the language of this instruction.';
54
+ }
27
55
 
28
56
  // --- Theme Loading ---
29
57
  function applyTheme(theme, accentColor, systemTextColor, config = {}) {
@@ -392,11 +420,17 @@ let currentAudioPlayer = null;
392
420
 
393
421
  function speakText(text, options = {}) {
394
422
  if (window.api && window.api.setAiState) window.api.setAiState('speaking');
395
- const onEnd = typeof options.onEnd === 'function' ? options.onEnd : null;
423
+ const onEnd = typeof options.onEnd === 'function' ? options.onEnd : () => {};
424
+
425
+ const wrappedOnEnd = () => {
426
+ if (window.Live2DManager) Live2DManager.stopLipSync();
427
+ onEnd();
428
+ };
429
+
396
430
  return new Promise(async (resolve) => {
397
431
  if (!enableVoiceReply) {
398
432
  if (window.api && window.api.setAiState) window.api.setAiState('idle');
399
- if (onEnd) onEnd();
433
+ wrappedOnEnd();
400
434
  return resolve();
401
435
  }
402
436
 
@@ -406,16 +440,20 @@ function speakText(text, options = {}) {
406
440
  currentAudioPlayer.currentTime = 0;
407
441
  currentAudioPlayer = null;
408
442
  }
443
+ if (window.Live2DManager) Live2DManager.stopLipSync();
444
+
409
445
  if ('speechSynthesis' in window) {
410
446
  window.speechSynthesis.cancel();
411
447
  }
412
448
 
413
449
  if (!text || !text.trim()) {
414
450
  if (window.api && window.api.setAiState) window.api.setAiState('idle');
415
- if (onEnd) onEnd();
451
+ wrappedOnEnd();
416
452
  return resolve();
417
453
  }
418
454
 
455
+ if (window.Live2DManager) Live2DManager.startLipSync();
456
+
419
457
  try {
420
458
  if (ttsProvider !== 'native') {
421
459
  const urls = await window.api.getTtsUrls(text);
@@ -424,7 +462,7 @@ function speakText(text, options = {}) {
424
462
  const playNext = () => {
425
463
  if (i >= urls.length) {
426
464
  if (window.api && window.api.setAiState) window.api.setAiState('idle');
427
- if (onEnd) onEnd();
465
+ wrappedOnEnd();
428
466
  return resolve();
429
467
  }
430
468
  const audio = new Audio(urls[i].url);
@@ -443,7 +481,7 @@ function speakText(text, options = {}) {
443
481
  };
444
482
  audio.play().catch(e => {
445
483
  console.error("Audio playback prevented:", e);
446
- fallbackSpeak(text, onEnd, resolve);
484
+ fallbackSpeak(text, wrappedOnEnd, resolve);
447
485
  });
448
486
  };
449
487
  playNext();
@@ -455,7 +493,7 @@ function speakText(text, options = {}) {
455
493
  }
456
494
 
457
495
  // Fallback
458
- fallbackSpeak(text, onEnd, resolve);
496
+ fallbackSpeak(text, wrappedOnEnd, resolve);
459
497
  });
460
498
  }
461
499
 
@@ -566,6 +604,95 @@ function formatProviderInfo(providerInfo) {
566
604
  return model ? `${provider || 'AI'} • ${model}` : provider;
567
605
  }
568
606
 
607
+ function splitListOutro(text) {
608
+ const value = String(text || '').trim();
609
+ const markers = [
610
+ ' คุณภีมอยาก',
611
+ ' อยากให้',
612
+ ' อยากดู',
613
+ ' บอกมิ้นท์',
614
+ ' Would you',
615
+ ' Do you want',
616
+ ' Tell me'
617
+ ];
618
+
619
+ for (const marker of markers) {
620
+ const index = value.indexOf(marker);
621
+ if (index > 60) {
622
+ return {
623
+ main: value.slice(0, index).trim(),
624
+ outro: value.slice(index).trim()
625
+ };
626
+ }
627
+ }
628
+
629
+ return { main: value, outro: '' };
630
+ }
631
+
632
+ function buildAiTextBlocks(text) {
633
+ const normalized = normalizeAiText(text).replace(/\r\n/g, '\n').trim();
634
+ if (!normalized) return [];
635
+
636
+ const readable = normalized
637
+ .replace(/\s+(\d+)[.)]\s+/g, '\n$1. ')
638
+ .replace(/\n{3,}/g, '\n\n');
639
+
640
+ const blocks = [];
641
+ const lines = readable.split(/\n+/).map(line => line.trim()).filter(Boolean);
642
+
643
+ for (const line of lines) {
644
+ const numbered = line.match(/^\d+[.)]\s+(.+)$/);
645
+ const bullet = line.match(/^[-*•]\s+(.+)$/);
646
+
647
+ if (numbered || bullet) {
648
+ const content = numbered ? numbered[1] : bullet[1];
649
+ const { main, outro } = splitListOutro(content);
650
+ blocks.push({ type: 'bullet', text: main });
651
+ if (outro) blocks.push({ type: 'paragraph', text: outro });
652
+ } else {
653
+ blocks.push({ type: 'paragraph', text: line });
654
+ }
655
+ }
656
+
657
+ return blocks;
658
+ }
659
+
660
+ function appendFormattedMessageText(bubble, text, sender) {
661
+ if (sender !== 'ai') {
662
+ const textSpan = document.createElement('span');
663
+ textSpan.textContent = text;
664
+ bubble.appendChild(textSpan);
665
+ return;
666
+ }
667
+
668
+ const blocks = buildAiTextBlocks(text);
669
+ if (blocks.length === 0) return;
670
+
671
+ const wrapper = document.createElement('div');
672
+ wrapper.classList.add('formatted-ai-text');
673
+
674
+ for (const block of blocks) {
675
+ const item = document.createElement(block.type === 'bullet' ? 'div' : 'p');
676
+ item.classList.add(block.type === 'bullet' ? 'ai-list-item' : 'ai-paragraph');
677
+
678
+ if (block.type === 'bullet') {
679
+ const bullet = document.createElement('span');
680
+ bullet.classList.add('ai-list-bullet');
681
+ bullet.textContent = '•';
682
+ const content = document.createElement('span');
683
+ content.textContent = block.text;
684
+ item.appendChild(bullet);
685
+ item.appendChild(content);
686
+ } else {
687
+ item.textContent = block.text;
688
+ }
689
+
690
+ wrapper.appendChild(item);
691
+ }
692
+
693
+ bubble.appendChild(wrapper);
694
+ }
695
+
569
696
  function appendMessage(text, sender, base64Image = null, timestamp = null, options = {}) {
570
697
  const messageDiv = document.createElement('div');
571
698
  messageDiv.classList.add('message', `${sender}-message`);
@@ -587,9 +714,7 @@ function appendMessage(text, sender, base64Image = null, timestamp = null, optio
587
714
  }
588
715
 
589
716
  if (text) {
590
- const textSpan = document.createElement('span');
591
- textSpan.textContent = text;
592
- bubble.appendChild(textSpan);
717
+ appendFormattedMessageText(bubble, text, sender);
593
718
  }
594
719
 
595
720
  bubbleWrapper.appendChild(bubble);
@@ -635,6 +760,9 @@ function normalizeAiText(input) {
635
760
  function splitAiMessages(text) {
636
761
  const normalized = normalizeAiText(text).trim();
637
762
  if (!normalized) return [];
763
+ if (/(^|\s)\d+[.)]\s+/.test(normalized) || /(^|\n)\s*[-*•]\s+/.test(normalized)) {
764
+ return [normalized];
765
+ }
638
766
  const byBlankLine = normalized
639
767
  .split(/\n\s*\n/)
640
768
  .map((part) => part.trim())
@@ -756,14 +884,52 @@ function scrollToBottom() {
756
884
  chatContainer.scrollTop = chatContainer.scrollHeight;
757
885
  }
758
886
 
887
+ function loadScript(src) {
888
+ return new Promise((resolve, reject) => {
889
+ if (document.querySelector(`script[src="${src}"]`)) {
890
+ resolve();
891
+ return;
892
+ }
893
+ const script = document.createElement('script');
894
+ script.src = src;
895
+ script.onload = resolve;
896
+ script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
897
+ document.body.appendChild(script);
898
+ });
899
+ }
900
+
901
+ async function loadLive2DWhenIdle() {
902
+ if (!modelMount || window.Live2DManager) return;
903
+ try {
904
+ await loadScript('../../node_modules/@hazart-pkg/live2d-core/live2dcubismcore.min.js');
905
+ await loadScript('../../node_modules/pixi.js/dist/browser/pixi.min.js');
906
+ await loadScript('../../node_modules/pixi-live2d-display/dist/cubism4.min.js');
907
+ await loadScript('live2d_manager.js');
908
+ if (window.Live2DManager) {
909
+ await Live2DManager.loadModel(modelMount, modelStatus, modelShell);
910
+ }
911
+ } catch (err) {
912
+ console.error('[Live2D] Deferred load failed:', err);
913
+ if (modelStatus) {
914
+ modelStatus.classList.add('is-error');
915
+ modelStatus.textContent = 'Live2D model unavailable.';
916
+ }
917
+ }
918
+ }
919
+
759
920
  async function loadChatHistory() {
760
921
  try {
761
922
  const history = await window.api.getChatHistory();
923
+ const initial = chatContainer.querySelector('.message.initial');
924
+
762
925
  if (!Array.isArray(history) || history.length === 0) {
926
+ if (initial) {
927
+ initial.style.display = 'flex';
928
+ initial.style.opacity = '1';
929
+ }
763
930
  return;
764
931
  }
765
932
 
766
- const initial = chatContainer.querySelector('.message.initial');
767
933
  if (initial) {
768
934
  initial.remove();
769
935
  }
@@ -771,11 +937,12 @@ async function loadChatHistory() {
771
937
  for (const item of history) {
772
938
  if (!item || typeof item.text !== 'string' || !item.text.trim()) continue;
773
939
  const sender = item.sender === 'user' ? 'user' : 'ai';
774
- if (sender === 'ai') {
775
- await appendAiMessages(item.text, { allowDelay: false, timestamp: item.timestamp, providerInfo: item.providerInfo });
776
- } else {
777
- appendMessage(item.text, sender, null, item.timestamp);
940
+ if (sender === 'user' && !String(item.text).startsWith('Model interaction:')) {
941
+ rememberConversationLanguage(item.text);
778
942
  }
943
+ appendMessage(item.text, sender, null, item.timestamp, {
944
+ providerInfo: sender === 'ai' ? item.providerInfo : null
945
+ });
779
946
  }
780
947
  } catch (error) {
781
948
  console.error('Failed to load chat history:', error);
@@ -785,23 +952,31 @@ async function loadChatHistory() {
785
952
  async function sendTextMessage(text, options = {}) {
786
953
  const cleanText = (text || '').trim();
787
954
  const allowSmartContext = options.allowSmartContext !== false;
955
+ const includePendingImage = options.includePendingImage !== false;
956
+ const displayText = options.displayText !== undefined ? options.displayText : cleanText;
957
+ const trackLanguage = options.trackLanguage !== false;
788
958
 
789
959
  // We can send either a text message, an image, or both.
790
- if (!cleanText && !currentBase64Image) return;
960
+ if (!cleanText && (!includePendingImage || !currentBase64Image)) return;
791
961
 
792
962
  // Cache the image for sending and UI, then clear
793
- let imageToSend = currentBase64Image;
963
+ let imageToSend = includePendingImage ? currentBase64Image : null;
794
964
 
795
965
  // Clear input & UI for explicit images
796
966
  chatInput.value = '';
797
- currentBase64Image = null;
798
- imagePreviewContainer.style.display = 'none';
799
- imagePreview.src = '';
967
+ if (includePendingImage) {
968
+ currentBase64Image = null;
969
+ imagePreviewContainer.style.display = 'none';
970
+ imagePreview.src = '';
971
+ }
800
972
 
801
973
  const now = new Date().toISOString();
802
974
 
803
975
  // Show user message (with explicit image if available)
804
- appendMessage(cleanText, 'user', imageToSend, now);
976
+ appendMessage(displayText, 'user', imageToSend, now);
977
+ if (trackLanguage) {
978
+ rememberConversationLanguage(displayText || cleanText);
979
+ }
805
980
 
806
981
  // Show typing early so user knows we are processing
807
982
  showTyping();
@@ -846,7 +1021,9 @@ async function sendTextMessage(text, options = {}) {
846
1021
  } else {
847
1022
  // General system info (date, time, RAM, CPU)
848
1023
  const info = await window.api.getSystemInfo();
849
- response.response += `\n\n📅 วันนี้: ${info.date}\n⏰ เวลา: ${info.time}\n💻 RAM: ${info.ram.used} / ${info.ram.total} (${info.ram.percent})`;
1024
+ const machine = info.machine && info.machine.display ? `\n🖥️ รุ่นเครื่อง: ${info.machine.display}` : '';
1025
+ const distro = info.distro ? `\nระบบ: ${info.distro}` : '';
1026
+ 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})`;
850
1027
  }
851
1028
  }
852
1029
 
@@ -880,6 +1057,20 @@ chatForm.addEventListener('submit', throttle(async (e) => {
880
1057
  await sendTextMessage(text);
881
1058
  }, 500));
882
1059
 
1060
+ window.addEventListener('live2d-model-interaction', async (event) => {
1061
+ const prompt = event?.detail?.prompt;
1062
+ if (!prompt) return;
1063
+ if (window.api && window.api.setAiState) window.api.setAiState('thinking');
1064
+ const interactionPrompt = `${prompt}\n\n${buildInteractionLanguageInstruction()}`;
1065
+ const displayPrefix = lastConversationLanguage === 'thai' ? 'แตะโมเดล' : 'Model interaction';
1066
+ await sendTextMessage(interactionPrompt, {
1067
+ allowSmartContext: false,
1068
+ includePendingImage: false,
1069
+ trackLanguage: false,
1070
+ displayText: `${displayPrefix}: ${event.detail.label || event.detail.region || 'Interaction'}`
1071
+ });
1072
+ });
1073
+
883
1074
  // --- Image Paste and Drag-n-Drop Support ---
884
1075
  function handleImageFile(file) {
885
1076
  if (!file || !file.type.startsWith('image/')) return;
@@ -936,6 +1127,8 @@ window.addEventListener('DOMContentLoaded', async () => {
936
1127
  chatInput.focus();
937
1128
  await loadTheme();
938
1129
  await loadChatHistory();
1130
+ const scheduleLive2DLoad = window.requestIdleCallback || ((callback) => setTimeout(callback, 750));
1131
+ scheduleLive2DLoad(() => loadLive2DWhenIdle());
939
1132
  });
940
1133
 
941
1134
  // Proactive OS Notifications (Battery, Network, etc.)
@@ -1028,6 +1221,131 @@ if (smartContextToggle) {
1028
1221
  });
1029
1222
  }
1030
1223
 
1224
+ // Toggle Live2D Model visibility
1225
+ const toggleModelBtn = document.getElementById('toggle-model-btn');
1226
+ const assistantWorkspace = document.querySelector('.assistant-workspace');
1227
+
1228
+ if (toggleModelBtn && assistantWorkspace) {
1229
+ 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
+ }
1244
+ });
1245
+
1246
+ // Restore preference on load
1247
+ const savedModelHidden = localStorage.getItem('mint-model-hidden');
1248
+ const savedHidden = savedModelHidden === null || savedModelHidden === 'true';
1249
+ if (savedHidden) {
1250
+ assistantWorkspace.classList.add('model-hidden');
1251
+ toggleModelBtn.classList.add('active');
1252
+ }
1253
+ }
1254
+
1255
+ // Cycle Shiroko's Expression
1256
+ const changeExpressionBtn = document.getElementById('change-expression-btn');
1257
+ if (changeExpressionBtn) {
1258
+ changeExpressionBtn.addEventListener('click', () => {
1259
+ if (window.Live2DManager) {
1260
+ Live2DManager.cycleExpression();
1261
+ }
1262
+ });
1263
+ }
1264
+
1265
+ // Cycle Live2D accessories
1266
+ const accessoryStorageKey = 'mint-live2d-accessories';
1267
+ const accessoryCycleBtn = document.getElementById('accessory-cycle-btn');
1268
+ const accessoryCycleLabel = document.getElementById('accessory-cycle-label');
1269
+ const accessoryCycleOrder = [null, 'glasses', 'pen', 'cat'];
1270
+ const accessoryLabels = {
1271
+ glasses: 'Glasses',
1272
+ pen: 'Pen',
1273
+ cat: 'Cat'
1274
+ };
1275
+ let savedAccessories = {};
1276
+ try {
1277
+ savedAccessories = JSON.parse(localStorage.getItem(accessoryStorageKey) || '{}') || {};
1278
+ } catch (_) {
1279
+ savedAccessories = {};
1280
+ }
1281
+
1282
+ const getSavedAccessoryId = () => accessoryCycleOrder.find(id => id && savedAccessories[id] === true) || null;
1283
+
1284
+ function updateAccessoryCycleButton(accessoryId) {
1285
+ if (!accessoryCycleBtn) return;
1286
+ const isActive = Boolean(accessoryId);
1287
+ const label = accessoryId ? accessoryLabels[accessoryId] : 'Accessory';
1288
+ accessoryCycleBtn.classList.toggle('active', isActive);
1289
+ accessoryCycleBtn.setAttribute('aria-pressed', String(isActive));
1290
+ accessoryCycleBtn.title = `Accessory: ${label}`;
1291
+ if (accessoryCycleLabel) accessoryCycleLabel.textContent = label;
1292
+ }
1293
+
1294
+ let currentAccessoryId = getSavedAccessoryId();
1295
+ updateAccessoryCycleButton(currentAccessoryId);
1296
+
1297
+ if (accessoryCycleBtn) {
1298
+ accessoryCycleBtn.addEventListener('click', () => {
1299
+ const currentIndex = accessoryCycleOrder.indexOf(currentAccessoryId);
1300
+ currentAccessoryId = accessoryCycleOrder[(currentIndex + 1) % accessoryCycleOrder.length];
1301
+ updateAccessoryCycleButton(currentAccessoryId);
1302
+
1303
+ if (window.Live2DManager) {
1304
+ Live2DManager.setExclusiveAccessory(currentAccessoryId, true);
1305
+ } else {
1306
+ savedAccessories = {};
1307
+ if (currentAccessoryId) savedAccessories[currentAccessoryId] = true;
1308
+ localStorage.setItem(accessoryStorageKey, JSON.stringify(savedAccessories));
1309
+ }
1310
+ });
1311
+ }
1312
+
1313
+ // Toggle Live2D model interaction
1314
+ const toggleInteractionBtn = document.getElementById('toggle-interaction-btn');
1315
+ if (toggleInteractionBtn) {
1316
+ const savedInteractionEnabled = localStorage.getItem('mint-model-interaction-enabled') !== 'false';
1317
+ toggleInteractionBtn.classList.toggle('active', savedInteractionEnabled);
1318
+ toggleInteractionBtn.setAttribute('aria-pressed', String(savedInteractionEnabled));
1319
+ if (window.Live2DManager) {
1320
+ Live2DManager.setInteractionEnabled(savedInteractionEnabled);
1321
+ }
1322
+
1323
+ toggleInteractionBtn.addEventListener('click', () => {
1324
+ const isEnabled = !toggleInteractionBtn.classList.contains('active');
1325
+ toggleInteractionBtn.classList.toggle('active', isEnabled);
1326
+ toggleInteractionBtn.setAttribute('aria-pressed', String(isEnabled));
1327
+ if (window.Live2DManager) {
1328
+ Live2DManager.setInteractionEnabled(isEnabled, true);
1329
+ } else {
1330
+ localStorage.setItem('mint-model-interaction-enabled', String(isEnabled));
1331
+ }
1332
+ });
1333
+ }
1334
+
1335
+ // Toggle Live2D interaction area guide
1336
+ const interactionGuideBtn = document.getElementById('interaction-guide-btn');
1337
+ if (interactionGuideBtn && modelShell) {
1338
+ const savedGuideVisible = localStorage.getItem('mint-interaction-guide-visible') === 'true';
1339
+ modelShell.classList.toggle('show-interaction-guide', savedGuideVisible);
1340
+ interactionGuideBtn.classList.toggle('active', savedGuideVisible);
1341
+
1342
+ interactionGuideBtn.addEventListener('click', () => {
1343
+ const isVisible = modelShell.classList.toggle('show-interaction-guide');
1344
+ interactionGuideBtn.classList.toggle('active', isVisible);
1345
+ localStorage.setItem('mint-interaction-guide-visible', String(isVisible));
1346
+ });
1347
+ }
1348
+
1031
1349
  // Spotlight integration
1032
1350
  window.api.onSpotlightToChat((query) => {
1033
1351
  chatInput.value = query;