@pheem49/mint 1.5.5 → 1.6.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 (222) hide show
  1. package/.codex +0 -0
  2. package/.github/FUNDING.yml +2 -0
  3. package/.github/workflows/ci.yml +45 -0
  4. package/.github/workflows/release.yml +79 -0
  5. package/Cargo.lock +5792 -0
  6. package/Cargo.toml +32 -0
  7. package/README.md +387 -353
  8. package/assets/icon.png +0 -0
  9. package/bin/mint +0 -0
  10. package/crates/mint-cli/Cargo.toml +23 -0
  11. package/crates/mint-cli/src/agent.rs +851 -0
  12. package/crates/mint-cli/src/gmail.rs +216 -0
  13. package/crates/mint-cli/src/image.rs +142 -0
  14. package/crates/mint-cli/src/main.rs +2837 -0
  15. package/crates/mint-cli/src/mcp.rs +63 -0
  16. package/crates/mint-cli/src/onboard.rs +1149 -0
  17. package/crates/mint-cli/src/setup.rs +390 -0
  18. package/crates/mint-cli/src/skills.rs +8 -0
  19. package/crates/mint-cli/src/updater.rs +279 -0
  20. package/crates/mint-core/Cargo.toml +22 -0
  21. package/crates/mint-core/src/agent_loop.rs +94 -0
  22. package/crates/mint-core/src/api_server.rs +991 -0
  23. package/crates/mint-core/src/channels.rs +248 -0
  24. package/crates/mint-core/src/chat.rs +895 -0
  25. package/crates/mint-core/src/code_tools.rs +729 -0
  26. package/crates/mint-core/src/config.rs +368 -0
  27. package/crates/mint-core/src/files.rs +159 -0
  28. package/crates/mint-core/src/knowledge.rs +541 -0
  29. package/crates/mint-core/src/lib.rs +84 -0
  30. package/crates/mint-core/src/mcp.rs +273 -0
  31. package/crates/mint-core/src/memory.rs +673 -0
  32. package/crates/mint-core/src/orchestration.rs +2157 -0
  33. package/crates/mint-core/src/pictures.rs +314 -0
  34. package/crates/mint-core/src/plugins.rs +727 -0
  35. package/crates/mint-core/src/safety.rs +416 -0
  36. package/crates/mint-core/src/semantic.rs +254 -0
  37. package/crates/mint-core/src/shell.rs +317 -0
  38. package/crates/mint-core/src/skills.rs +71 -0
  39. package/crates/mint-core/src/symbols.rs +157 -0
  40. package/crates/mint-core/src/tasks.rs +308 -0
  41. package/crates/mint-core/src/tts.rs +92 -0
  42. package/crates/mint-core/src/weather.rs +93 -0
  43. package/crates/mint-core/src/web_search.rs +200 -0
  44. package/crates/mint-core/src/workflows.rs +81 -0
  45. package/crates/mint-core/tests/mcp_stdio.rs +45 -0
  46. package/crates/mint-core/tests/memory_persistence.rs +172 -0
  47. package/crates/mint-core/tests/pictures_storage.rs +14 -0
  48. package/crates/mint-core/tests/task_lifecycle.rs +87 -0
  49. package/package.json +35 -99
  50. package/src/bin/index.js +16 -0
  51. package/src/renderer/index-web.html +17 -0
  52. package/src/renderer/index.html +17 -0
  53. package/src/renderer/public/Live2DCubismCore.js +9 -0
  54. package/src/renderer/public/assets/icon.png +0 -0
  55. package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
  56. package/src/renderer/src/App.tsx +33 -0
  57. package/src/renderer/src/calculator.ts +47 -0
  58. package/src/renderer/src/components/ChatPanel.tsx +1598 -0
  59. package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
  60. package/src/renderer/src/components/Live2DStage.tsx +374 -0
  61. package/src/renderer/src/components/MintDashboard.tsx +950 -0
  62. package/src/renderer/src/components/ModelPanel.tsx +154 -0
  63. package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
  64. package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
  65. package/src/renderer/src/components/ScreenPicker.tsx +579 -0
  66. package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
  67. package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
  68. package/src/renderer/src/components/WidgetWindow.tsx +36 -0
  69. package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
  70. package/src/{UI → renderer/src/css}/settings.css +69 -16
  71. package/src/renderer/src/css/spotlight.css +113 -0
  72. package/src/renderer/src/css/styles.css +3722 -0
  73. package/src/renderer/src/css/widget.css +185 -0
  74. package/src/renderer/src/env.d.ts +116 -0
  75. package/src/renderer/src/index.css +379 -0
  76. package/src/renderer/src/main.tsx +13 -0
  77. package/src/renderer/src/tauri.ts +996 -0
  78. package/src/renderer/src-web/App.tsx +25 -0
  79. package/src/renderer/src-web/calculator.ts +47 -0
  80. package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
  81. package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
  82. package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
  83. package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
  84. package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
  85. package/src/renderer/src-web/css/settings.css +1100 -0
  86. package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
  87. package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
  88. package/src/{UI → renderer/src-web/css}/widget.css +2 -2
  89. package/src/renderer/src-web/env.d.ts +107 -0
  90. package/src/renderer/src-web/index.css +379 -0
  91. package/src/renderer/src-web/main.tsx +13 -0
  92. package/src/renderer/src-web/tauri.ts +983 -0
  93. package/tsconfig.json +30 -0
  94. package/vite.config.ts +33 -0
  95. package/vite.config.web.ts +51 -0
  96. package/GUIDE_TH.md +0 -125
  97. package/assets/Agent_Mint.png +0 -0
  98. package/assets/CLI_Screen.png +0 -0
  99. package/assets/Settings.png +0 -0
  100. package/benchmark_ai.js +0 -71
  101. package/install.ps1 +0 -64
  102. package/install.sh +0 -54
  103. package/main.js +0 -139
  104. package/mint-cli-logic.js +0 -3
  105. package/mint-cli.js +0 -410
  106. package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
  107. 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 +0 -23
  108. package/preload-picker.js +0 -11
  109. package/preload-settings.js +0 -11
  110. package/preload.js +0 -41
  111. package/scripts/install_linux_desktop_entry.js +0 -48
  112. package/src/AI_Brain/Gemini_API.js +0 -813
  113. package/src/AI_Brain/agent_orchestrator.js +0 -73
  114. package/src/AI_Brain/autonomous_brain.js +0 -179
  115. package/src/AI_Brain/behavior_memory.js +0 -135
  116. package/src/AI_Brain/headless_agent.js +0 -143
  117. package/src/AI_Brain/knowledge_base.js +0 -349
  118. package/src/AI_Brain/memory_store.js +0 -662
  119. package/src/AI_Brain/proactive_engine.js +0 -172
  120. package/src/AI_Brain/provider_adapter.js +0 -365
  121. package/src/Automation_Layer/browser_automation.js +0 -149
  122. package/src/Automation_Layer/file_operations.js +0 -286
  123. package/src/Automation_Layer/open_app.js +0 -85
  124. package/src/Automation_Layer/open_website.js +0 -38
  125. package/src/CLI/approval_handler.js +0 -47
  126. package/src/CLI/chat_router.js +0 -247
  127. package/src/CLI/chat_ui.js +0 -1159
  128. package/src/CLI/cli_colors.js +0 -115
  129. package/src/CLI/cli_formatters.js +0 -94
  130. package/src/CLI/code_agent.js +0 -1667
  131. package/src/CLI/code_session_memory.js +0 -62
  132. package/src/CLI/gmail_auth.js +0 -210
  133. package/src/CLI/image_input.js +0 -90
  134. package/src/CLI/intent_detectors.js +0 -181
  135. package/src/CLI/interactive_chat.js +0 -658
  136. package/src/CLI/list_features.js +0 -64
  137. package/src/CLI/onboarding.js +0 -416
  138. package/src/CLI/repo_summarizer.js +0 -282
  139. package/src/CLI/semantic_code_search.js +0 -312
  140. package/src/CLI/skill_manager.js +0 -41
  141. package/src/CLI/slash_command_handler.js +0 -418
  142. package/src/CLI/symbol_indexer.js +0 -231
  143. package/src/CLI/updater.js +0 -230
  144. package/src/CLI/workspace_manager.js +0 -90
  145. package/src/Channels/brave_search_bridge.js +0 -35
  146. package/src/Channels/discord_bridge.js +0 -66
  147. package/src/Channels/google_search_bridge.js +0 -38
  148. package/src/Channels/line_bridge.js +0 -60
  149. package/src/Channels/slack_bridge.js +0 -48
  150. package/src/Channels/telegram_bridge.js +0 -41
  151. package/src/Channels/whatsapp_bridge.js +0 -57
  152. package/src/Command_Parser/parser.js +0 -45
  153. package/src/Plugins/dev_tools.js +0 -41
  154. package/src/Plugins/discord.js +0 -20
  155. package/src/Plugins/docker.js +0 -47
  156. package/src/Plugins/gmail.js +0 -251
  157. package/src/Plugins/google_calendar.js +0 -252
  158. package/src/Plugins/mcp_manager.js +0 -95
  159. package/src/Plugins/notion.js +0 -256
  160. package/src/Plugins/obsidian.js +0 -54
  161. package/src/Plugins/plugin_manager.js +0 -81
  162. package/src/Plugins/spotify.js +0 -173
  163. package/src/Plugins/system_metrics.js +0 -31
  164. package/src/Plugins/system_monitor.js +0 -72
  165. package/src/System/action_executor.js +0 -178
  166. package/src/System/bridge_manager.js +0 -76
  167. package/src/System/chat_history_manager.js +0 -83
  168. package/src/System/config_manager.js +0 -194
  169. package/src/System/custom_workflows.js +0 -163
  170. package/src/System/daemon_manager.js +0 -67
  171. package/src/System/google_tts_urls.js +0 -51
  172. package/src/System/granular_automation.js +0 -157
  173. package/src/System/ipc_handlers.js +0 -332
  174. package/src/System/notifications.js +0 -23
  175. package/src/System/optional_require.js +0 -23
  176. package/src/System/picture_store.js +0 -109
  177. package/src/System/proactive_loop.js +0 -153
  178. package/src/System/safety_manager.js +0 -273
  179. package/src/System/sandbox_runner.js +0 -182
  180. package/src/System/screen_capture.js +0 -175
  181. package/src/System/smart_context.js +0 -227
  182. package/src/System/system_automation.js +0 -162
  183. package/src/System/system_events.js +0 -79
  184. package/src/System/system_info.js +0 -125
  185. package/src/System/task_manager.js +0 -222
  186. package/src/System/tool_registry.js +0 -293
  187. package/src/System/window_manager.js +0 -220
  188. package/src/UI/floating.css +0 -80
  189. package/src/UI/floating.html +0 -17
  190. package/src/UI/floating.js +0 -67
  191. package/src/UI/live2d_manager.js +0 -600
  192. package/src/UI/preload-floating.js +0 -7
  193. package/src/UI/preload-spotlight.js +0 -11
  194. package/src/UI/preload-widget.js +0 -5
  195. package/src/UI/proactive-glow.html +0 -42
  196. package/src/UI/renderer.js +0 -2127
  197. package/src/UI/screenPicker.html +0 -214
  198. package/src/UI/screenPicker.js +0 -262
  199. package/src/UI/settings.html +0 -577
  200. package/src/UI/settings.js +0 -770
  201. package/src/UI/spotlight.html +0 -23
  202. package/src/UI/spotlight.js +0 -185
  203. package/src/UI/widget.html +0 -29
  204. package/src/UI/widget.js +0 -10
  205. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
  206. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
  207. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
  208. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
  209. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
  210. /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" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
  211. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
  212. /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
  213. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
  214. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
  215. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
  216. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
  217. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
  218. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
  219. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
  220. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
  221. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
  222. /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
@@ -0,0 +1,950 @@
1
+ import { type ChangeEvent, type FormEvent, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ clearChatHistory,
4
+ deleteChatSession,
5
+ renameChatSession,
6
+ getRecentInteractions,
7
+ getRuntimeStatus,
8
+ listChatSessions,
9
+ listSavedPictures,
10
+ selectWorkspaceDirectory,
11
+ streamChatMessage,
12
+ submitToolApproval,
13
+ listen,
14
+ readClipboardImage as readTauriClipboardImage,
15
+ type AgentProgress,
16
+ type ChatResponse,
17
+ type ChatSession,
18
+ type DocumentAttachment,
19
+ type PictureEntry,
20
+ type RuntimeStatus,
21
+ } from '../tauri'
22
+ import ChatPanel from './ChatPanel'
23
+ import DashboardSidebar, { type DashboardView } from './DashboardSidebar'
24
+ import ModelPanel from './ModelPanel'
25
+ import type { ModelInteraction } from './ModelPanel'
26
+ import PicturesLibrary from './PicturesLibrary'
27
+ import WorkspacePanel from './WorkspacePanel'
28
+
29
+ const EXPRESSIONS = [
30
+ "ปกติ (Default)",
31
+ "呆猫 (Dumb Cat)",
32
+ "呆猫眼珠摇晃 (Dumb Cat Eye Roll)",
33
+ "拍照 (Take Photo)",
34
+ "点一下 (Poke)",
35
+ "猫咪滤镜 (Cat Filter)",
36
+ ]
37
+
38
+ const ACCESSORIES = [
39
+ "ปกติ (None)",
40
+ "ผ้ากันเปื้อน (Apron)",
41
+ "แว่นตา (Glasses)",
42
+ "ท่าถือปากกา (Hold Pen)",
43
+ ]
44
+
45
+ const DEFAULT_CONFIG = {
46
+ theme: 'dark',
47
+ accentColor: '#4f83e6',
48
+ systemTextColor: '#f8fafc',
49
+ customBgStart: '#0f172a',
50
+ customBgEnd: '#1e1b4b',
51
+ customPanelBg: '#1e293b',
52
+ glassBlur: 'blur(16px)',
53
+ fontFamily: "'Outfit', sans-serif",
54
+ fontSize: '15px',
55
+ }
56
+
57
+ const LAST_WORKSPACE_PATH_KEY = 'mint:last-workspace-path'
58
+ const ACTIVE_CONVERSATION_ID_KEY = 'mint:active-conversation-id'
59
+
60
+ function createConversationId() {
61
+ const random = Math.random().toString(36).slice(2, 10)
62
+ return `conversation-${Date.now().toString(36)}-${random}`
63
+ }
64
+
65
+ function activeConversationId() {
66
+ const existing = window.localStorage.getItem(ACTIVE_CONVERSATION_ID_KEY)
67
+ if (existing === 'conversation-default') {
68
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, 'cli')
69
+ return 'cli'
70
+ }
71
+ if (existing) return existing
72
+ const next = createConversationId()
73
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, next)
74
+ return next
75
+ }
76
+
77
+ const MOCK_WELCOME_INTERACTION = {
78
+ id: -1,
79
+ userText: '',
80
+ aiText: `มิ้นท์กำลังรอสแตนด์บายเตรียมพร้อมช่วยคุณทีมอยู่เลยค่ะ! ✨ แล้วก็แอบนั่งจัดระเบียบข้อมูลนิดๆ หน่อยๆ ให้พร้อมใช้ด้วยค่ะ 😊💖\n\nแต่พอคุณทีมทักมา มิ้นท์ก็วางมือจากทุกอย่างมาคุยกับคุณทีมก่อนเลยนะคะค้าา! ช่วงนี้มีอะไรให้มิ้นท์ช่วยดูแล หรืออยากชวนคุยเรื่องไหนเป็นพิเศษไหมคะ มิ้นท์พร้อมมว๊ากกกค่ะ! 🚀🎯`,
81
+ provider: 'gemini',
82
+ model: 'gemini-3-flash-preview',
83
+ createdAt: new Date().toISOString(),
84
+ }
85
+
86
+ function errorMessage(reason: unknown) {
87
+ return reason instanceof Error ? reason.message : String(reason)
88
+ }
89
+
90
+ function readImage(file: File): Promise<string> {
91
+ return new Promise((resolve, reject) => {
92
+ const reader = new FileReader()
93
+ reader.onload = () => resolve(String(reader.result))
94
+ reader.onerror = () => reject(reader.error ?? new Error('Unable to read image'))
95
+ reader.readAsDataURL(file)
96
+ })
97
+ }
98
+
99
+ function readDocument(file: File): Promise<string> {
100
+ return new Promise((resolve, reject) => {
101
+ const reader = new FileReader()
102
+ reader.onload = () => resolve(String(reader.result))
103
+ reader.onerror = () => reject(reader.error ?? new Error('Unable to read document'))
104
+ reader.readAsDataURL(file)
105
+ })
106
+ }
107
+
108
+ async function createTrimmedImagePreview(dataUri: string): Promise<string> {
109
+ const image = await new Promise<HTMLImageElement>((resolve, reject) => {
110
+ const nextImage = new Image()
111
+ nextImage.onload = () => resolve(nextImage)
112
+ nextImage.onerror = () => reject(new Error('Unable to prepare image preview'))
113
+ nextImage.src = dataUri
114
+ })
115
+
116
+ const canvas = document.createElement('canvas')
117
+ canvas.width = image.naturalWidth || image.width
118
+ canvas.height = image.naturalHeight || image.height
119
+ const context = canvas.getContext('2d', { willReadFrequently: true })
120
+ if (!context || canvas.width === 0 || canvas.height === 0) return dataUri
121
+
122
+ context.drawImage(image, 0, 0)
123
+ const pixels = context.getImageData(0, 0, canvas.width, canvas.height)
124
+ let minX = canvas.width
125
+ let minY = canvas.height
126
+ let maxX = -1
127
+ let maxY = -1
128
+
129
+ for (let y = 0; y < canvas.height; y += 1) {
130
+ for (let x = 0; x < canvas.width; x += 1) {
131
+ const alpha = pixels.data[(y * canvas.width + x) * 4 + 3]
132
+ if (alpha > 12) {
133
+ minX = Math.min(minX, x)
134
+ minY = Math.min(minY, y)
135
+ maxX = Math.max(maxX, x)
136
+ maxY = Math.max(maxY, y)
137
+ }
138
+ }
139
+ }
140
+
141
+ if (maxX < minX || maxY < minY) return dataUri
142
+
143
+ const padding = 8
144
+ const sx = Math.max(0, minX - padding)
145
+ const sy = Math.max(0, minY - padding)
146
+ const sw = Math.min(canvas.width - sx, maxX - minX + 1 + padding * 2)
147
+ const sh = Math.min(canvas.height - sy, maxY - minY + 1 + padding * 2)
148
+
149
+ if (sw >= canvas.width * 0.92 && sh >= canvas.height * 0.92) return dataUri
150
+
151
+ const previewCanvas = document.createElement('canvas')
152
+ previewCanvas.width = sw
153
+ previewCanvas.height = sh
154
+ const previewContext = previewCanvas.getContext('2d')
155
+ if (!previewContext) return dataUri
156
+ previewContext.drawImage(canvas, sx, sy, sw, sh, 0, 0, sw, sh)
157
+ return previewCanvas.toDataURL('image/png')
158
+ }
159
+
160
+ const lightenColor = (hex: string, amount: number) => {
161
+ const clean = hex.replace('#', '')
162
+ if (clean.length !== 6) return hex
163
+ const num = parseInt(clean, 16)
164
+ const r = Math.min(255, (num >> 16) + amount)
165
+ const g = Math.min(255, ((num >> 8) & 0x00FF) + amount)
166
+ const b = Math.min(255, (num & 0x0000FF) + amount)
167
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`
168
+ }
169
+
170
+ const hexToRgb = (hex: string) => {
171
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
172
+ return result ? {
173
+ r: parseInt(result[1], 16),
174
+ g: parseInt(result[2], 16),
175
+ b: parseInt(result[3], 16),
176
+ } : { r: 15, g: 23, b: 42 }
177
+ }
178
+
179
+ const applyThemeStyles = (cfg: any) => {
180
+ const theme = cfg.theme || 'dark'
181
+ const accentColor = cfg.accentColor || '#4f83e6'
182
+ const systemTextColor = cfg.systemTextColor || '#f8fafc'
183
+
184
+ document.documentElement.setAttribute('data-theme', theme)
185
+ document.documentElement.style.setProperty('--accent', accentColor)
186
+ document.documentElement.style.setProperty('--accent-hover', lightenColor(accentColor, 20))
187
+ document.documentElement.style.setProperty('--text-main', systemTextColor)
188
+ document.documentElement.style.setProperty('--glass-blur', cfg.glassBlur || 'blur(16px)')
189
+ document.body.style.fontFamily = cfg.fontFamily || "'Outfit', sans-serif"
190
+ document.documentElement.style.fontSize = cfg.fontSize || '15px'
191
+
192
+ if (theme === 'custom') {
193
+ if (cfg.customBgStart && cfg.customBgEnd) {
194
+ document.documentElement.style.setProperty('--bg-color', cfg.customBgStart)
195
+ document.documentElement.style.setProperty('--bg-gradient', `linear-gradient(135deg, ${cfg.customBgStart} 0%, ${cfg.customBgEnd} 100%)`)
196
+ }
197
+ if (cfg.customPanelBg) {
198
+ const rgb = hexToRgb(cfg.customPanelBg)
199
+ document.documentElement.style.setProperty('--panel-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.75)`)
200
+ document.documentElement.style.setProperty('--panel-raised', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.82)`)
201
+ document.documentElement.style.setProperty('--panel-soft', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.46)`)
202
+ document.documentElement.style.setProperty('--chrome-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.88)`)
203
+ document.documentElement.style.setProperty('--surface-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.62)`)
204
+ document.documentElement.style.setProperty('--surface-strong', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.86)`)
205
+ document.documentElement.style.setProperty('--input-bg', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.72)`)
206
+ }
207
+ return
208
+ }
209
+
210
+ ;[
211
+ '--bg-color',
212
+ '--bg-gradient',
213
+ '--panel-bg',
214
+ '--panel-raised',
215
+ '--panel-soft',
216
+ '--chrome-bg',
217
+ '--surface-bg',
218
+ '--surface-strong',
219
+ '--input-bg',
220
+ ].forEach((name) => document.documentElement.style.removeProperty(name))
221
+ }
222
+
223
+ export default function MintDashboard() {
224
+ const [view, setView] = useState<DashboardView>('chat')
225
+ const [status, setStatus] = useState<RuntimeStatus | null>(null)
226
+ const [error, setError] = useState('')
227
+ const [message, setMessage] = useState('')
228
+ const [interactions, setInteractions] = useState<any[]>([])
229
+ const [pictures, setPictures] = useState<PictureEntry[]>([])
230
+ const [sending, setSending] = useState(false)
231
+ const [sendingMessage, setSendingMessage] = useState('')
232
+ const [sendingImageCount, setSendingImageCount] = useState(0)
233
+ const [streamedReply, setStreamedReply] = useState('')
234
+ const [streamedResponse, setStreamedResponse] = useState<ChatResponse | null>(null)
235
+ const [agentProgress, setAgentProgress] = useState<AgentProgress[]>([])
236
+ const [agentActivitySnapshots, setAgentActivitySnapshots] = useState<Record<string, AgentProgress[]>>({})
237
+ const [imageAttachments, setImageAttachments] = useState<Array<{ dataUri: string; name: string; previewDataUri?: string }>>([])
238
+ const [documentAttachment, setDocumentAttachment] = useState<DocumentAttachment | null>(null)
239
+ const [pendingApproval, setPendingApproval] = useState<any | null>(null)
240
+ const [sessionAutoApproved, setSessionAutoApproved] = useState(false)
241
+ const sessionAutoApprovedRef = useRef(false)
242
+ const [modelVisible, setModelVisible] = useState(() => window.localStorage.getItem('mint:model-visible') !== 'false')
243
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('mint:sidebar-collapsed') === 'true')
244
+ const [smartContext, setSmartContext] = useState(() => window.localStorage.getItem('mint:smart-context') !== 'false')
245
+ const [agentMode, setAgentMode] = useState(() => window.localStorage.getItem('mint:agent-mode') === 'true')
246
+ const [scale, setScale] = useState(1.00)
247
+ const [interactionEnabled, setInteractionEnabled] = useState(() => window.localStorage.getItem('mint:interaction-enabled') !== 'false')
248
+ const [showInteractionGuide, setShowInteractionGuide] = useState(() => window.localStorage.getItem('mint:interaction-guide-visible') !== 'false')
249
+ const [isLocked, setIsLocked] = useState(false)
250
+ const [layoutPreset, setLayoutPreset] = useState<'chat-wide' | 'model-wide'>(() => (window.localStorage.getItem('mint:layout-preset') as 'chat-wide' | 'model-wide') || 'chat-wide')
251
+ const [toastMessage, setToastMessage] = useState('')
252
+ const [expressionIndex, setExpressionIndex] = useState(0)
253
+ const [accessoryIndex, setAccessoryIndex] = useState(0)
254
+ const [dashboardDataReady, setDashboardDataReady] = useState(false)
255
+ const [modelReady, setModelReady] = useState(false)
256
+ const [startupTimedOut, setStartupTimedOut] = useState(false)
257
+ const [settingsConfig, setSettingsConfig] = useState<any>(null)
258
+ const [workspacePath, setWorkspacePath] = useState(() => window.localStorage.getItem(LAST_WORKSPACE_PATH_KEY) || '')
259
+ const [conversationId, setConversationId] = useState(activeConversationId)
260
+ const [chatSessions, setChatSessions] = useState<ChatSession[]>([])
261
+ const chatEnd = useRef<HTMLDivElement | null>(null)
262
+ const startupReady = (dashboardDataReady && modelReady) || startupTimedOut
263
+
264
+ async function refreshHistory() {
265
+ const history = await getRecentInteractions(50, conversationId)
266
+ setInteractions(history.reverse())
267
+ }
268
+
269
+ async function refreshChatSessions(nextActiveId = conversationId) {
270
+ const sessions = await listChatSessions()
271
+ const isKnown = sessions.some((session) => session.id === nextActiveId)
272
+ setChatSessions(
273
+ isKnown || nextActiveId === 'cli'
274
+ ? sessions
275
+ : [
276
+ {
277
+ id: nextActiveId,
278
+ title: 'New chat',
279
+ kind: 'conversation',
280
+ createdAt: new Date().toISOString(),
281
+ updatedAt: new Date().toISOString(),
282
+ },
283
+ ...sessions,
284
+ ],
285
+ )
286
+ }
287
+
288
+ async function refreshPictures() {
289
+ setPictures(await listSavedPictures())
290
+ }
291
+
292
+ useEffect(() => {
293
+ Promise.allSettled([
294
+ getRuntimeStatus().then(setStatus),
295
+ refreshHistory(),
296
+ refreshChatSessions(),
297
+ window.settingsApi?.getSettings()
298
+ .then((loaded: any) => {
299
+ setSettingsConfig(loaded)
300
+ applyThemeStyles({ ...DEFAULT_CONFIG, ...loaded })
301
+ }),
302
+ ]).then((results) => {
303
+ const failure = results.find((result) => result.status === 'rejected')
304
+ if (failure?.status === 'rejected') setError(errorMessage(failure.reason))
305
+ setDashboardDataReady(true)
306
+ })
307
+ const unlistenSpotlight = window.api.onSpotlightToChat((query) => {
308
+ setView('chat')
309
+ setMessage(query)
310
+ })
311
+ const unlistenVision = window.api.onVisionReady((image) => {
312
+ createTrimmedImagePreview(image)
313
+ .catch(() => image)
314
+ .then((previewDataUri) => {
315
+ setImageAttachments((current) => [...current, { dataUri: image, previewDataUri, name: 'Screen capture' }])
316
+ })
317
+ })
318
+ window.api?.onSettingsChanged?.((loaded: any) => {
319
+ setSettingsConfig(loaded)
320
+ applyThemeStyles(loaded)
321
+ })
322
+
323
+ const unlistenPromise = listen<any>('tool-approval-requested', (event) => {
324
+ if (sessionAutoApprovedRef.current) {
325
+ submitToolApproval(event.payload.token, true).catch((err) => {
326
+ console.error("Auto approval failed:", err)
327
+ })
328
+ } else {
329
+ setPendingApproval(event.payload)
330
+ }
331
+ })
332
+ return () => {
333
+ unlistenPromise?.then?.((unlisten) => unlisten?.())
334
+ unlistenSpotlight?.then?.((unlisten) => unlisten?.())
335
+ unlistenVision?.then?.((unlisten) => unlisten?.())
336
+ }
337
+ }, [])
338
+
339
+ useEffect(() => {
340
+ const timer = window.setTimeout(() => setStartupTimedOut(true), 10000)
341
+ return () => window.clearTimeout(timer)
342
+ }, [])
343
+
344
+ useEffect(() => {
345
+ if (view === 'pictures') refreshPictures().catch((reason: unknown) => setError(errorMessage(reason)))
346
+ }, [view])
347
+
348
+ useEffect(() => {
349
+ if (view === 'workspeac' && !agentMode) updateAgentMode(true)
350
+ }, [view, agentMode])
351
+
352
+ useEffect(() => {
353
+ chatEnd.current?.scrollIntoView({ behavior: 'smooth' })
354
+ }, [interactions, sending, streamedReply, pendingApproval, agentProgress])
355
+
356
+ const showToast = (nextMessage: string) => {
357
+ setToastMessage(nextMessage)
358
+ setTimeout(() => setToastMessage((current) => current === nextMessage ? '' : current), 3000)
359
+ }
360
+
361
+ const changeLayoutPreset = (preset: 'chat-wide' | 'model-wide') => {
362
+ window.localStorage.setItem('mint:layout-preset', preset)
363
+ setLayoutPreset(preset)
364
+ }
365
+
366
+ const toggleModel = () => {
367
+ const next = !modelVisible
368
+ window.localStorage.setItem('mint:model-visible', String(next))
369
+ setModelVisible(next)
370
+ }
371
+
372
+ const toggleSidebar = () => {
373
+ const next = !sidebarCollapsed
374
+ window.localStorage.setItem('mint:sidebar-collapsed', String(next))
375
+ setSidebarCollapsed(next)
376
+ }
377
+
378
+ const updateInteractionEnabled = (enabled: boolean) => {
379
+ window.localStorage.setItem('mint:interaction-enabled', String(enabled))
380
+ setInteractionEnabled(enabled)
381
+ }
382
+
383
+ const updateInteractionGuide = (visible: boolean) => {
384
+ window.localStorage.setItem('mint:interaction-guide-visible', String(visible))
385
+ setShowInteractionGuide(visible)
386
+ }
387
+
388
+ const updateSmartContext = (enabled: boolean) => {
389
+ window.localStorage.setItem('mint:smart-context', String(enabled))
390
+ setSmartContext(enabled)
391
+ }
392
+
393
+ const updateAgentMode = (enabled: boolean) => {
394
+ window.localStorage.setItem('mint:agent-mode', String(enabled))
395
+ setAgentMode(enabled)
396
+ }
397
+
398
+ const updateWorkspacePath = (path: string) => {
399
+ const next = path.trim()
400
+ if (next) {
401
+ window.localStorage.setItem(LAST_WORKSPACE_PATH_KEY, next)
402
+ } else {
403
+ window.localStorage.removeItem(LAST_WORKSPACE_PATH_KEY)
404
+ }
405
+ setWorkspacePath(next)
406
+ }
407
+
408
+ useEffect(() => {
409
+ if (!sending) {
410
+ sessionAutoApprovedRef.current = false
411
+ setSessionAutoApproved(false)
412
+ }
413
+ }, [sending])
414
+
415
+ async function handleApproval(approved: boolean, autoApproveSession = false) {
416
+ if (!pendingApproval) return
417
+ try {
418
+ if (autoApproveSession) {
419
+ sessionAutoApprovedRef.current = true
420
+ setSessionAutoApproved(true)
421
+ }
422
+ await submitToolApproval(pendingApproval.token, approved)
423
+ } catch (reason) {
424
+ setError(errorMessage(reason))
425
+ } finally {
426
+ setPendingApproval(null)
427
+ }
428
+ }
429
+
430
+ async function sendPrompt(
431
+ promptText: string,
432
+ options: {
433
+ imageAttachments?: Array<{ dataUri: string; name: string; previewDataUri?: string }>
434
+ audioDataUri?: string | null
435
+ documentAttachment?: DocumentAttachment | null
436
+ systemInstruction?: string
437
+ clearComposer?: boolean
438
+ } = {},
439
+ ) {
440
+ if (sending) return
441
+ const outgoingImages = options.imageAttachments ?? []
442
+ const outgoingDocument = options.documentAttachment ?? null
443
+ const shouldUseAgentMode = agentMode || promptText.toLowerCase().startsWith('search web:')
444
+ const outgoingImage = outgoingImages.map((img) => img.dataUri).join(' ')
445
+ const outgoingImageCount = outgoingImages.length
446
+ setSending(true)
447
+ setSendingMessage(promptText)
448
+ setSendingImageCount(outgoingImageCount)
449
+ setError('')
450
+ setStreamedReply('')
451
+ setStreamedResponse(null)
452
+ setAgentProgress([])
453
+ const progressSnapshot: AgentProgress[] = []
454
+ if (options.clearComposer) {
455
+ setMessage('')
456
+ setImageAttachments([])
457
+ setDocumentAttachment(null)
458
+ }
459
+
460
+ try {
461
+ const response = await streamChatMessage(
462
+ shouldUseAgentMode ? promptText : `/chat ${promptText}`,
463
+ (chunk) => setStreamedReply((current) => `${current}${chunk}`),
464
+ outgoingImage,
465
+ options.audioDataUri ?? null,
466
+ options.systemInstruction ?? '',
467
+ (progress) => {
468
+ progressSnapshot.push(progress)
469
+ setAgentProgress((current) => [...current, progress].slice(-24))
470
+ },
471
+ outgoingDocument,
472
+ workspacePath || null,
473
+ conversationId,
474
+ )
475
+ setStreamedResponse(response)
476
+ const history = (await getRecentInteractions(50, conversationId)).reverse()
477
+ if (progressSnapshot.length > 0) {
478
+ const newestInteraction = [...history]
479
+ .reverse()
480
+ .find((interaction) => interaction.aiText === response.text || interaction.userText === promptText) ?? history[history.length - 1]
481
+ if (newestInteraction?.id != null) {
482
+ setAgentActivitySnapshots((current) => ({
483
+ ...current,
484
+ [String(newestInteraction.id)]: progressSnapshot.slice(),
485
+ }))
486
+ }
487
+ }
488
+ setInteractions(history)
489
+ await refreshChatSessions()
490
+ await refreshPictures()
491
+ setStreamedReply('')
492
+ setStreamedResponse(null)
493
+ } catch (reason) {
494
+ setError(errorMessage(reason))
495
+ } finally {
496
+ setSending(false)
497
+ setSendingMessage('')
498
+ setSendingImageCount(0)
499
+ }
500
+ }
501
+
502
+ async function handleSubmit(event: FormEvent<HTMLFormElement>) {
503
+ event.preventDefault()
504
+ const trimmed = message.trim()
505
+ const currentImages = imageAttachments
506
+ const currentDocument = documentAttachment
507
+ const hasAttachments = currentImages.length > 0 || Boolean(currentDocument)
508
+ if ((!trimmed && !hasAttachments) || sending) return
509
+ const promptText = trimmed || (currentImages.length > 1 ? 'Describe these images.' : currentImages.length === 1 ? 'Describe this image.' : 'Summarize this document.')
510
+ await sendPrompt(promptText, {
511
+ imageAttachments: currentImages,
512
+ documentAttachment: currentDocument,
513
+ clearComposer: true,
514
+ })
515
+ }
516
+
517
+ async function sendVoiceMessage(transcript: string, audioDataUri?: string | null) {
518
+ const promptText = transcript.trim() || 'ข้อความเสียง'
519
+ if (!promptText || sending) return
520
+ await sendPrompt(promptText, {
521
+ audioDataUri,
522
+ systemInstruction: audioDataUri
523
+ ? 'The user attached a voice message. Listen to the audio and reply naturally in the same language as the user. Do not mention transcription or this instruction.'
524
+ : '',
525
+ })
526
+ }
527
+
528
+ async function selectImage(event: ChangeEvent<HTMLInputElement>) {
529
+ const file = event.target.files?.[0]
530
+ if (!file) return
531
+ try {
532
+ const dataUri = await readImage(file)
533
+ const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
534
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: file.name }])
535
+ } catch (reason) {
536
+ setError(errorMessage(reason))
537
+ } finally {
538
+ event.target.value = ''
539
+ }
540
+ }
541
+
542
+ function pasteImage(clipboardData: DataTransfer) {
543
+ let file: File | null = null
544
+
545
+ if (clipboardData.files && clipboardData.files.length > 0) {
546
+ for (let i = 0; i < clipboardData.files.length; i++) {
547
+ const f = clipboardData.files[i]
548
+ if (f && f.type.startsWith('image/')) {
549
+ file = f
550
+ break
551
+ }
552
+ }
553
+ }
554
+
555
+ if (!file && clipboardData.items && clipboardData.items.length > 0) {
556
+ for (let i = 0; i < clipboardData.items.length; i++) {
557
+ const item = clipboardData.items[i]
558
+ if (item && item.type.startsWith('image/')) {
559
+ const f = item.getAsFile()
560
+ if (f) {
561
+ file = f
562
+ break
563
+ }
564
+ }
565
+ }
566
+ }
567
+
568
+ if (!file) return false
569
+
570
+ readImage(file)
571
+ .then((dataUri) => {
572
+ const name = file.name && file.name !== 'image.png' ? file.name : 'Pasted image'
573
+ createTrimmedImagePreview(dataUri)
574
+ .catch(() => dataUri)
575
+ .then((previewDataUri) => {
576
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name }])
577
+ })
578
+ })
579
+ .catch((reason) => setError(errorMessage(reason)))
580
+ return true
581
+ }
582
+
583
+ async function readClipboardImage() {
584
+ try {
585
+ const dataUri = await readTauriClipboardImage()
586
+ if (dataUri) {
587
+ const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
588
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
589
+ return true
590
+ }
591
+ } catch (err) {
592
+ console.warn('Tauri clipboard fallback error:', err)
593
+ }
594
+
595
+ try {
596
+ if (!navigator.clipboard?.read) return false
597
+ const items = await navigator.clipboard.read()
598
+ for (const item of items) {
599
+ const imageType = item.types.find((type) => type.startsWith('image/'))
600
+ if (!imageType) continue
601
+ const blob = await item.getType(imageType)
602
+ const file = new File([blob], 'Pasted image', { type: imageType })
603
+ const dataUri = await readImage(file)
604
+ const previewDataUri = await createTrimmedImagePreview(dataUri).catch(() => dataUri)
605
+ setImageAttachments((current) => [...current, { dataUri, previewDataUri, name: 'Pasted image' }])
606
+ return true
607
+ }
608
+ return false
609
+ } catch {
610
+ // Some environments expose pasted images only through ClipboardEvent.
611
+ return false
612
+ }
613
+ }
614
+
615
+ async function selectDocument(event: ChangeEvent<HTMLInputElement>) {
616
+ const file = event.target.files?.[0]
617
+ if (!file) return
618
+ try {
619
+ if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
620
+ throw new Error('Only PDF files are supported')
621
+ }
622
+ setDocumentAttachment({
623
+ filename: file.name,
624
+ dataUri: await readDocument(file),
625
+ })
626
+ } catch (reason) {
627
+ setError(errorMessage(reason))
628
+ } finally {
629
+ event.target.value = ''
630
+ }
631
+ }
632
+
633
+ function startWebSearch() {
634
+ updateAgentMode(true)
635
+ setMessage((current) => current.trim() ? `Search web: ${current.trim()}` : 'Search web: ')
636
+ }
637
+
638
+ async function selectWorkspace() {
639
+ try {
640
+ const selected = await selectWorkspaceDirectory()
641
+ if (selected) {
642
+ updateWorkspacePath(selected)
643
+ setView('workspeac')
644
+ }
645
+ } catch (reason) {
646
+ setError(errorMessage(reason))
647
+ }
648
+ }
649
+
650
+ async function captureScreen() {
651
+ try {
652
+ await window.api.startVision()
653
+ } catch (reason) {
654
+ setError(errorMessage(reason))
655
+ }
656
+ }
657
+
658
+ async function clearHistory(action: 'New chat' | 'Clear history') {
659
+ try {
660
+ if (action === 'New chat') {
661
+ const next = createConversationId()
662
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, next)
663
+ setConversationId(next)
664
+ await refreshChatSessions(next)
665
+ } else {
666
+ if (!window.confirm(`${action} will clear the current conversation history. Continue?`)) return
667
+ await clearChatHistory(conversationId)
668
+ }
669
+ setInteractions([])
670
+ setAgentActivitySnapshots({})
671
+ setStreamedReply('')
672
+ setStreamedResponse(null)
673
+ setMessage('')
674
+ setImageAttachments([])
675
+ } catch (reason) {
676
+ setError(errorMessage(reason))
677
+ }
678
+ }
679
+
680
+ async function selectConversation(id: string) {
681
+ if (id === conversationId) {
682
+ setView('chat')
683
+ return
684
+ }
685
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, id)
686
+ setConversationId(id)
687
+ setView('chat')
688
+ setStreamedReply('')
689
+ setStreamedResponse(null)
690
+ setMessage('')
691
+ setImageAttachments([])
692
+ setDocumentAttachment(null)
693
+ setAgentProgress([])
694
+ const history = await getRecentInteractions(50, id)
695
+ setInteractions(history.reverse())
696
+ }
697
+
698
+ async function deleteConversation(id: string) {
699
+ if (id === 'cli') return
700
+ const session = chatSessions.find((item) => item.id === id)
701
+ const title = session?.title || 'this chat'
702
+ if (!window.confirm(`Delete "${title}"? This will remove the conversation and its messages.`)) return
703
+
704
+ try {
705
+ await deleteChatSession(id)
706
+ const remaining = chatSessions.filter((item) => item.id !== id && item.kind !== 'cli' && item.id !== 'conversation-default')
707
+ const nextActive = id === conversationId
708
+ ? (remaining[0]?.id ?? createConversationId())
709
+ : conversationId
710
+
711
+ if (nextActive !== conversationId) {
712
+ window.localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, nextActive)
713
+ setConversationId(nextActive)
714
+ setAgentProgress([])
715
+ const history = await getRecentInteractions(50, nextActive)
716
+ setInteractions(history.reverse())
717
+ }
718
+
719
+ await refreshChatSessions(nextActive)
720
+ if (id !== conversationId) return
721
+ setStreamedReply('')
722
+ setStreamedResponse(null)
723
+ setMessage('')
724
+ setImageAttachments([])
725
+ setDocumentAttachment(null)
726
+ } catch (reason) {
727
+ setError(errorMessage(reason))
728
+ }
729
+ }
730
+
731
+ async function renameConversation(id: string, newTitle: string) {
732
+ if (!newTitle.trim()) return
733
+ try {
734
+ await renameChatSession(id, newTitle.trim())
735
+ await refreshChatSessions(conversationId)
736
+ } catch (reason) {
737
+ setError(errorMessage(reason))
738
+ }
739
+ }
740
+
741
+ async function changeProvider(provider: string) {
742
+ try {
743
+ const config = await window.settingsApi.getSettings()
744
+ config.aiProvider = provider
745
+ await window.settingsApi.saveSettings(config)
746
+ setSettingsConfig(config)
747
+ setStatus(await getRuntimeStatus())
748
+ } catch (reason) {
749
+ setError(errorMessage(reason))
750
+ }
751
+ }
752
+
753
+ async function changeModel(modelName: string) {
754
+ try {
755
+ const config = await window.settingsApi.getSettings()
756
+ const provider = config.aiProvider
757
+ if (provider === 'gemini') {
758
+ config.geminiModel = modelName
759
+ } else if (provider === 'openai') {
760
+ config.openaiModel = modelName
761
+ } else if (provider === 'openrouter') {
762
+ config.openrouterModel = modelName
763
+ } else if (provider === 'deepseek') {
764
+ config.deepseekModel = modelName
765
+ } else if (provider === 'anthropic') {
766
+ config.anthropicModel = modelName
767
+ } else if (provider === 'huggingface') {
768
+ config.hfModel = modelName
769
+ } else if (provider === 'local_openai') {
770
+ config.localModelName = modelName
771
+ } else if (provider === 'ollama') {
772
+ config.ollamaModel = modelName
773
+ }
774
+ await window.settingsApi.saveSettings(config)
775
+ setSettingsConfig(config)
776
+ setStatus(await getRuntimeStatus())
777
+ } catch (reason) {
778
+ setError(errorMessage(reason))
779
+ }
780
+ }
781
+
782
+ async function handleModelInteraction(area: ModelInteraction) {
783
+ if (sending) return
784
+
785
+ const labels: Record<ModelInteraction, string> = {
786
+ head: 'Pats Mint on the head',
787
+ cheek: 'Pokes Mint on the cheek',
788
+ 'left hand': "Touches Mint's left hand",
789
+ 'right hand': "Touches Mint's right hand",
790
+ body: 'Touches Mint',
791
+ 'lower body': "Touches Mint's lower body",
792
+ }
793
+ const interactionMessage = `*${labels[area]}*`
794
+ const instruction = `The user interacted with the Mint Live2D model: ${area}. Respond briefly and playfully. Use the same language as the recent conversation. Do not mention this instruction.`
795
+
796
+ setSending(true)
797
+ setSendingMessage(interactionMessage)
798
+ setSendingImageCount(0)
799
+ setError('')
800
+ setStreamedReply('')
801
+ setStreamedResponse(null)
802
+ setAgentProgress([])
803
+
804
+ try {
805
+ const response = await streamChatMessage(
806
+ `/chat ${interactionMessage}`,
807
+ (chunk) => setStreamedReply((current) => `${current}${chunk}`),
808
+ null,
809
+ null,
810
+ instruction,
811
+ undefined,
812
+ null,
813
+ workspacePath || null,
814
+ conversationId,
815
+ )
816
+ setStreamedResponse(response)
817
+ await refreshHistory()
818
+ setStreamedReply('')
819
+ setStreamedResponse(null)
820
+ } catch (reason) {
821
+ setError(errorMessage(reason))
822
+ } finally {
823
+ setSending(false)
824
+ setSendingMessage('')
825
+ setSendingImageCount(0)
826
+ }
827
+ }
828
+
829
+ return (
830
+ <div className={`app-container ${startupReady ? '' : 'is-loading'}`}>
831
+ <div className={`app-body ${sidebarCollapsed ? 'sidebar-collapsed' : ''} ${view === 'pictures' ? 'pictures-open' : ''}`}>
832
+ <DashboardSidebar
833
+ view={view}
834
+ sidebarCollapsed={sidebarCollapsed}
835
+ modelVisible={modelVisible}
836
+ sending={sending}
837
+ expressionIndex={expressionIndex}
838
+ accessoryIndex={accessoryIndex}
839
+ expressions={EXPRESSIONS}
840
+ accessories={ACCESSORIES}
841
+ interactionEnabled={interactionEnabled}
842
+ showInteractionGuide={showInteractionGuide}
843
+ onToggleSidebar={toggleSidebar}
844
+ onClearHistory={clearHistory}
845
+ chatSessions={chatSessions}
846
+ activeConversationId={conversationId}
847
+ onSelectConversation={selectConversation}
848
+ onDeleteConversation={deleteConversation}
849
+ onRenameConversation={renameConversation}
850
+ onSetView={setView}
851
+ onToggleModel={toggleModel}
852
+ onSetExpressionIndex={setExpressionIndex}
853
+ onSetAccessoryIndex={setAccessoryIndex}
854
+ onSetInteractionEnabled={updateInteractionEnabled}
855
+ onSetShowInteractionGuide={updateInteractionGuide}
856
+ onShowToast={showToast}
857
+ />
858
+ <main className={`assistant-workspace ${layoutPreset === 'chat-wide' ? 'layout-chat-wide' : 'layout-model-wide'} ${modelVisible || view === 'workspeac' ? '' : 'model-hidden'} ${view === 'workspeac' ? 'workspace-open' : ''}`}>
859
+ {view === 'workspeac' && (
860
+ <WorkspacePanel
861
+ agentMode={agentMode}
862
+ sending={sending}
863
+ workspacePath={workspacePath}
864
+ onEnableAgentMode={() => updateAgentMode(true)}
865
+ onSetMessage={setMessage}
866
+ onWorkspaceReady={updateWorkspacePath}
867
+ />
868
+ )}
869
+ <ModelPanel
870
+ scale={scale}
871
+ expressionIndex={expressionIndex}
872
+ accessoryIndex={accessoryIndex}
873
+ isLocked={isLocked}
874
+ isActive={modelVisible && view !== 'pictures' && view !== 'workspeac'}
875
+ layoutPreset={layoutPreset}
876
+ sending={sending}
877
+ interactionEnabled={interactionEnabled}
878
+ showInteractionGuide={showInteractionGuide}
879
+ toastMessage={toastMessage}
880
+ onSetScale={setScale}
881
+ onSetLocked={setIsLocked}
882
+ onSetView={setView}
883
+ onChangeLayoutPreset={changeLayoutPreset}
884
+ onDismissToast={() => setToastMessage('')}
885
+ onInteract={handleModelInteraction}
886
+ onModelLoadComplete={() => setModelReady(true)}
887
+ />
888
+ <ChatPanel
889
+ interactions={interactions}
890
+ sending={sending}
891
+ sendingMessage={sendingMessage}
892
+ sendingImageCount={sendingImageCount}
893
+ streamedReply={streamedReply}
894
+ streamedResponse={streamedResponse}
895
+ agentProgress={agentProgress}
896
+ agentActivitySnapshots={agentActivitySnapshots}
897
+ message={message}
898
+ imageAttachments={imageAttachments}
899
+ documentName={documentAttachment?.filename ?? ''}
900
+ pendingApproval={pendingApproval}
901
+ smartContext={smartContext}
902
+ agentMode={agentMode}
903
+ status={status}
904
+ workspacePath={workspacePath}
905
+ chatEnd={chatEnd}
906
+ welcomeInteraction={MOCK_WELCOME_INTERACTION}
907
+ onSubmit={handleSubmit}
908
+ onSelectImage={selectImage}
909
+ onSelectDocument={selectDocument}
910
+ onPasteImage={pasteImage}
911
+ onReadClipboardImage={readClipboardImage}
912
+ onSetMessage={setMessage}
913
+ onSendVoiceMessage={sendVoiceMessage}
914
+ onRemoveImage={(idx: number) => {
915
+ setImageAttachments((current) => current.filter((_, i) => i !== idx))
916
+ }}
917
+ onRemoveDocument={() => setDocumentAttachment(null)}
918
+ onStartWebSearch={startWebSearch}
919
+ onCaptureScreen={captureScreen}
920
+ onSetSmartContext={updateSmartContext}
921
+ onSetAgentMode={updateAgentMode}
922
+ onSetProvider={changeProvider}
923
+ onSelectWorkspace={selectWorkspace}
924
+ settingsConfig={settingsConfig}
925
+ onSetModel={changeModel}
926
+ onApproval={handleApproval}
927
+ />
928
+ </main>
929
+ <PicturesLibrary view={view} pictures={pictures} onSetView={setView} />
930
+ </div>
931
+ <div className={`startup-loading ${startupReady ? 'is-hidden' : ''}`} aria-live="polite" aria-busy={!startupReady}>
932
+ <div className="startup-loading-content">
933
+ <div className="startup-loading-dots" aria-hidden="true"><span /><span /><span /></div>
934
+ <div className="startup-loading-text">Loading Agent Mint</div>
935
+ </div>
936
+ </div>
937
+ {error && (
938
+ <div className="mint-error" style={{ position: 'absolute', bottom: '20px', right: '20px', zIndex: 100, margin: 0, boxShadow: '0 8px 24px rgba(0,0,0,0.3)', display: 'flex', alignItems: 'center', gap: '8px' }}>
939
+ <span>{error}</span>
940
+ <button onClick={() => setError('')} style={{ background: 'transparent', border: 0, color: 'white', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', padding: 0 }}>
941
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
942
+ <line x1="18" y1="6" x2="6" y2="18"></line>
943
+ <line x1="6" y1="6" x2="18" y2="18"></line>
944
+ </svg>
945
+ </button>
946
+ </div>
947
+ )}
948
+ </div>
949
+ )
950
+ }