@mazees/mark 5.0.0

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 (227) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +209 -0
  3. package/bin/mark.js +27 -0
  4. package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
  5. package/out/renderer/assets/index-G2fADWO-.js +4768 -0
  6. package/out/renderer/assets/index-f3mVse3c.css +1 -0
  7. package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
  8. package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  9. package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  10. package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
  11. package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
  12. package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
  13. package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
  14. package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
  15. package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
  16. package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  17. package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  18. package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  19. package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  20. package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  21. package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  22. package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  23. package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  24. package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  25. package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  26. package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
  27. package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
  28. package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
  29. package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
  30. package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
  31. package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
  32. package/out/renderer/favicon.ico +0 -0
  33. package/out/renderer/icon-256.png +0 -0
  34. package/out/renderer/icon.svg +1 -0
  35. package/out/renderer/index.html +25 -0
  36. package/out/renderer/manifest.json +25 -0
  37. package/out/renderer/robot-simulator.html +1640 -0
  38. package/package.json +106 -0
  39. package/src/cli/index.js +332 -0
  40. package/src/cli/monitor.js +413 -0
  41. package/src/cli/theme.js +347 -0
  42. package/src/launcher/MarkTray.cs +227 -0
  43. package/src/main/browser-agent.js +1132 -0
  44. package/src/main/git-service.js +90 -0
  45. package/src/main/google/google-calendar.js +69 -0
  46. package/src/main/google/google-drive.js +263 -0
  47. package/src/main/google/google-gmail.js +131 -0
  48. package/src/main/google/google-service.js +241 -0
  49. package/src/main/node-tools.js +23 -0
  50. package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
  51. package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
  52. package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
  53. package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
  54. package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
  55. package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
  56. package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
  57. package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
  58. package/src/main/plugins/plugin-loader.js +306 -0
  59. package/src/main/skills/skill-manager.js +265 -0
  60. package/src/main/syntax-validator.js +455 -0
  61. package/src/main/task-daemon.js +117 -0
  62. package/src/main/telegram/telegram-service.js +835 -0
  63. package/src/main/tools/browser-tools.js +516 -0
  64. package/src/main/tools/file-tools.js +1126 -0
  65. package/src/main/tools/git-tools.js +79 -0
  66. package/src/main/tools/google-tools.js +566 -0
  67. package/src/main/tools/index.js +36 -0
  68. package/src/main/tools/media-tools.js +42 -0
  69. package/src/main/tools/system-tools.js +295 -0
  70. package/src/main/tools/task-tools.js +73 -0
  71. package/src/main/tools/telegram-tools.js +101 -0
  72. package/src/renderer/index.html +24 -0
  73. package/src/renderer/public/favicon.ico +0 -0
  74. package/src/renderer/public/icon-256.png +0 -0
  75. package/src/renderer/public/icon.svg +1 -0
  76. package/src/renderer/public/manifest.json +25 -0
  77. package/src/renderer/public/robot-simulator.html +1640 -0
  78. package/src/renderer/src/App.jsx +304 -0
  79. package/src/renderer/src/api/ai/awareness.js +294 -0
  80. package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
  81. package/src/renderer/src/api/ai/contextCompactor.js +129 -0
  82. package/src/renderer/src/api/ai/contextManager.js +1130 -0
  83. package/src/renderer/src/api/ai/core.js +167 -0
  84. package/src/renderer/src/api/ai/index.js +4 -0
  85. package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
  86. package/src/renderer/src/api/ai/persona.js +115 -0
  87. package/src/renderer/src/api/ai/planning.js +317 -0
  88. package/src/renderer/src/api/ai/relationship.js +177 -0
  89. package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
  90. package/src/renderer/src/api/ai/tools.js +167 -0
  91. package/src/renderer/src/api/ai/utils.js +359 -0
  92. package/src/renderer/src/api/db.js +1013 -0
  93. package/src/renderer/src/api/oramaStore.js +592 -0
  94. package/src/renderer/src/api/ragPipeline.js +87 -0
  95. package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
  96. package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
  97. package/src/renderer/src/api/subagent/subagentStore.js +173 -0
  98. package/src/renderer/src/api/taskExecutor.js +18 -0
  99. package/src/renderer/src/api/taskStore.js +315 -0
  100. package/src/renderer/src/api/tools/core-tools.js +707 -0
  101. package/src/renderer/src/api/tools/index.js +151 -0
  102. package/src/renderer/src/api/turnPairMigrator.js +200 -0
  103. package/src/renderer/src/api/vectorMemory.js +136 -0
  104. package/src/renderer/src/api/wakeWord.js +101 -0
  105. package/src/renderer/src/api/web-bridge.js +885 -0
  106. package/src/renderer/src/api/webSpeech.js +178 -0
  107. package/src/renderer/src/api/workspaceRag.js +76 -0
  108. package/src/renderer/src/assets/electron.svg +10 -0
  109. package/src/renderer/src/assets/icon.svg +1 -0
  110. package/src/renderer/src/assets/main.css +589 -0
  111. package/src/renderer/src/assets/music-cover.png +0 -0
  112. package/src/renderer/src/assets/wavy-lines.svg +25 -0
  113. package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
  114. package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
  115. package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
  116. package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
  117. package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
  118. package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
  119. package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
  120. package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
  121. package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
  122. package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
  123. package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
  124. package/src/renderer/src/components/Chat/index.js +11 -0
  125. package/src/renderer/src/components/ChatList.jsx +434 -0
  126. package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
  127. package/src/renderer/src/components/Versions.jsx +15 -0
  128. package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
  129. package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
  130. package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
  131. package/src/renderer/src/components/config/CameraSection.jsx +100 -0
  132. package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
  133. package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
  134. package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
  135. package/src/renderer/src/components/config/index.js +5 -0
  136. package/src/renderer/src/components/core/Avatar.jsx +2197 -0
  137. package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
  138. package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
  139. package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
  140. package/src/renderer/src/components/core/HoloCard.jsx +64 -0
  141. package/src/renderer/src/components/core/InputBar.jsx +1346 -0
  142. package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
  143. package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
  144. package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
  145. package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
  146. package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
  147. package/src/renderer/src/components/core/native-skills.js +43 -0
  148. package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
  149. package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
  150. package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
  151. package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
  152. package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
  153. package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
  154. package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
  155. package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
  156. package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
  157. package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
  158. package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
  159. package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
  160. package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
  161. package/src/renderer/src/contexts/ChatContext.jsx +61 -0
  162. package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
  163. package/src/renderer/src/data/faqData.js +202 -0
  164. package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
  165. package/src/renderer/src/hooks/agent/index.js +4 -0
  166. package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
  167. package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
  168. package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
  169. package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
  170. package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
  171. package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
  172. package/src/renderer/src/hooks/useAwareness.js +289 -0
  173. package/src/renderer/src/hooks/useChatArchiver.js +52 -0
  174. package/src/renderer/src/hooks/useConfirm.jsx +68 -0
  175. package/src/renderer/src/hooks/useMarkAgent.js +454 -0
  176. package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
  177. package/src/renderer/src/hooks/useVAD.js +511 -0
  178. package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
  179. package/src/renderer/src/main.jsx +12 -0
  180. package/src/renderer/src/pages/ChatStudio.jsx +922 -0
  181. package/src/renderer/src/pages/Configuration.jsx +360 -0
  182. package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
  183. package/src/renderer/src/pages/Guidebook.jsx +917 -0
  184. package/src/renderer/src/pages/Knowledge.jsx +241 -0
  185. package/src/renderer/src/pages/MarkHome.jsx +491 -0
  186. package/src/renderer/src/pages/NeuralCore.jsx +786 -0
  187. package/src/renderer/src/pages/Plugins.jsx +630 -0
  188. package/src/renderer/src/pages/SkillEditor.jsx +380 -0
  189. package/src/renderer/src/pages/Skills.jsx +219 -0
  190. package/src/renderer/src/pages/Subagents.jsx +342 -0
  191. package/src/renderer/src/pages/TelegramBot.jsx +343 -0
  192. package/src/renderer/src/utils/chatContentParser.js +203 -0
  193. package/src/renderer/src/utils/toolClusters.js +201 -0
  194. package/src/server/bin/sha3_wasm_bg.wasm +0 -0
  195. package/src/server/config-manager.js +57 -0
  196. package/src/server/index.js +195 -0
  197. package/src/server/launcher.js +87 -0
  198. package/src/server/memory/db-store.js +758 -0
  199. package/src/server/memory/orama-store.js +177 -0
  200. package/src/server/memory/vector-engine.js +109 -0
  201. package/src/server/routes/ai.routes.js +280 -0
  202. package/src/server/routes/awareness.routes.js +62 -0
  203. package/src/server/routes/chat.routes.js +61 -0
  204. package/src/server/routes/config.routes.js +41 -0
  205. package/src/server/routes/index.js +35 -0
  206. package/src/server/routes/integrations.routes.js +116 -0
  207. package/src/server/routes/memory.routes.js +256 -0
  208. package/src/server/routes/plugins.routes.js +72 -0
  209. package/src/server/routes/skills.routes.js +139 -0
  210. package/src/server/routes/subagents.routes.js +70 -0
  211. package/src/server/routes/tasks.routes.js +175 -0
  212. package/src/server/routes/workspace.routes.js +127 -0
  213. package/src/server/services/ai/ai-utils.js +161 -0
  214. package/src/server/services/ai/openai-provider.js +757 -0
  215. package/src/server/services/ai/web-provider.js +729 -0
  216. package/src/server/services/ai-bridge.js +46 -0
  217. package/src/server/services/deepseek-web.js +956 -0
  218. package/src/server/services/gemini-web.js +170 -0
  219. package/src/server/services/notification-service.js +91 -0
  220. package/src/server/services/updater.js +125 -0
  221. package/src/server/tools/awareness-tracker.js +292 -0
  222. package/src/server/tools/group-tools.js +1164 -0
  223. package/src/server/tools/media-tools.js +244 -0
  224. package/src/server/tools/pc-agent.js +327 -0
  225. package/src/server/tools/screen-service.js +51 -0
  226. package/src/server/ws-hub.js +168 -0
  227. package/src/shared/parsers/mark-tag-parser.js +275 -0
@@ -0,0 +1,922 @@
1
+ import React, { useState, useEffect, useRef } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { useChat } from '../contexts/ChatContext'
4
+ import {
5
+ MessageSquare,
6
+ Plus,
7
+ Trash2,
8
+ Edit2,
9
+ Search,
10
+ Pin,
11
+ X,
12
+ Maximize2,
13
+ Minimize2,
14
+ Sparkles,
15
+ Check,
16
+ RotateCcw,
17
+ Send,
18
+ Loader2,
19
+ ArrowLeft,
20
+ Bot,
21
+ Folder
22
+ } from 'lucide-react'
23
+ import {
24
+ db,
25
+ getAllSessions,
26
+ getSession,
27
+ createSession,
28
+ saveSession,
29
+ deleteSession,
30
+ renameSession,
31
+ getChatData,
32
+ setSessionWorkspace,
33
+ getSessionCompact
34
+ } from '../api/db'
35
+ import ChatList from '../components/ChatList'
36
+ import InputBar from '../components/core/InputBar'
37
+ import { useConfirm } from '../hooks/useConfirm'
38
+ import { stripMarkTags } from '@shared/parsers/mark-tag-parser.js'
39
+
40
+ /**
41
+ * Ekstraksi dan filter semua prefix/meta-tag (seperti <mic>, <mood:value>)
42
+ * serta fallback riwayat lama ((Mikrofon), [mood:...]) agar pesan tampil bersih di ChatStudio.
43
+ *
44
+ * @param {string|Array} content
45
+ * @returns {{ cleanContent: string|Array, prefixes: string[] }}
46
+ */
47
+ export function filterMessagePrefixes(content) {
48
+ if (!content) return { cleanContent: '', prefixes: [] }
49
+ if (typeof content !== 'string') {
50
+ if (Array.isArray(content)) {
51
+ const allPrefixes = []
52
+ const cleanArray = content.map((item) => {
53
+ if (item && item.type === 'text' && typeof item.text === 'string') {
54
+ const res = filterMessagePrefixes(item.text)
55
+ allPrefixes.push(...res.prefixes)
56
+ return { ...item, text: res.cleanContent }
57
+ }
58
+ return item
59
+ })
60
+ return { cleanContent: cleanArray, prefixes: allPrefixes }
61
+ }
62
+ return { cleanContent: content, prefixes: [] }
63
+ }
64
+
65
+ const prefixes = []
66
+ const cleanContent = stripMarkTags(content)
67
+ .replace(/(?:<mic>|\(Mikrofon\))/gi, (match) => {
68
+ prefixes.push(match)
69
+ return ''
70
+ })
71
+ .trim()
72
+
73
+ return { cleanContent, prefixes }
74
+ }
75
+
76
+ export const ChatStudio = ({ isOpen, onClose, chatContext: propChatContext }) => {
77
+ const navigate = useNavigate()
78
+ const contextChat = useChat()
79
+ const chatContext = propChatContext || contextChat
80
+ const {
81
+ chatData: mainChatData,
82
+ setChatData: setMainChatData,
83
+ handlePlanningCommand,
84
+ handleIntervention,
85
+ isLoading: isMainLoading,
86
+ isAgentBusy,
87
+ runningSessionId,
88
+ runningSessionIds = [],
89
+ handleStop,
90
+ isRecording,
91
+ isProcessing,
92
+ audioIntensity,
93
+ startRecording,
94
+ stopRecording,
95
+ inputSource,
96
+ setCurrentActiveSessionId
97
+ } = chatContext || {}
98
+
99
+ const [sessions, setSessions] = useState([])
100
+ const [activeSessionId, setActiveSessionId] = useState('1')
101
+ const [lastCompactedMessageId, setLastCompactedMessageId] = useState(null)
102
+ const [activeSessionCompact, setActiveSessionCompact] = useState(null)
103
+ const [loading, setLoading] = useState(true)
104
+
105
+ useEffect(() => {
106
+ if (typeof setCurrentActiveSessionId === 'function') {
107
+ setCurrentActiveSessionId(String(activeSessionId))
108
+ }
109
+ return () => {
110
+ if (typeof setCurrentActiveSessionId === 'function') {
111
+ setCurrentActiveSessionId('1')
112
+ }
113
+ }
114
+ }, [activeSessionId, setCurrentActiveSessionId])
115
+
116
+ useEffect(() => {
117
+ let isMounted = true
118
+ const loadCompactData = async () => {
119
+ try {
120
+ const data = await getSessionCompact(String(activeSessionId))
121
+ if (isMounted) {
122
+ setActiveSessionCompact(data || null)
123
+ setLastCompactedMessageId(
124
+ data?.lastCompactedMessageId || data?.last_compacted_message_id || null
125
+ )
126
+ }
127
+ } catch (_) {}
128
+ }
129
+ loadCompactData()
130
+
131
+ const handleCompactUpdated = (e) => {
132
+ if (String(e.detail?.sessionId) === String(activeSessionId)) {
133
+ setLastCompactedMessageId(e.detail?.lastCompactedMessageId || null)
134
+ setActiveSessionCompact({
135
+ summaryBlock: e.detail?.summaryBlock,
136
+ lastCompactedMessageId: e.detail?.lastCompactedMessageId
137
+ })
138
+ }
139
+ }
140
+
141
+ window.addEventListener('session-compact-updated', handleCompactUpdated)
142
+ return () => {
143
+ isMounted = false
144
+ window.removeEventListener('session-compact-updated', handleCompactUpdated)
145
+ }
146
+ }, [activeSessionId])
147
+
148
+ const [activeSessionData, setActiveSessionData] = useState([])
149
+ const [visibleMessageCount, setVisibleMessageCount] = useState(40)
150
+ const [searchQuery, setSearchQuery] = useState('')
151
+ const [editingSessionId, setEditingSessionId] = useState(null)
152
+ const [editingTitle, setEditingTitle] = useState('')
153
+
154
+ const messagesContainerRef = useRef(null)
155
+ const messagesEndRef = useRef(null)
156
+ const { confirm, ModalComponent } = useConfirm()
157
+
158
+ const loadAllSessions = async () => {
159
+ try {
160
+ const list = await getAllSessions()
161
+ setSessions(list || [])
162
+ } catch (e) {
163
+ console.error('Error loading sessions:', e)
164
+ }
165
+ }
166
+
167
+ useEffect(() => {
168
+ let isMounted = true
169
+ const initStudio = async () => {
170
+ try {
171
+ setLoading(true)
172
+ await loadAllSessions()
173
+ } catch (err) {
174
+ console.error('Error loading sessions:', err)
175
+ } finally {
176
+ if (isMounted) setLoading(false)
177
+ }
178
+ }
179
+ initStudio()
180
+ return () => {
181
+ isMounted = false
182
+ }
183
+ }, [])
184
+
185
+ // Direct display pipeline: Main Thread uses mainChatData directly with 0ms lag
186
+ const currentDisplayMessages =
187
+ String(activeSessionId) === '1' ? mainChatData || [] : activeSessionData
188
+
189
+ const isCurrentLoading =
190
+ runningSessionIds.map(String).includes(String(activeSessionId)) ||
191
+ (String(activeSessionId) === '1' && !runningSessionIds.length && (isMainLoading || isAgentBusy))
192
+
193
+ // Sync active session data for custom sessions (id > 1)
194
+ useEffect(() => {
195
+ setVisibleMessageCount(30)
196
+ if (String(activeSessionId) === '1') return
197
+ let isCancelled = false
198
+ getChatData(activeSessionId).then((data) => {
199
+ if (!isCancelled) {
200
+ setActiveSessionData(data || [])
201
+ }
202
+ })
203
+ return () => {
204
+ isCancelled = true
205
+ }
206
+ }, [activeSessionId])
207
+
208
+ // Sinkronisasi status karakter gauge ke UI saat sesi aktif dimuat/berganti
209
+ useEffect(() => {
210
+ if (!currentDisplayMessages || currentDisplayMessages.length === 0) {
211
+ window.dispatchEvent(
212
+ new CustomEvent('context-tracker-updated', {
213
+ detail: {
214
+ sessionId: String(activeSessionId),
215
+ currentTokens: 0,
216
+ maxTokens: 256000,
217
+ currentChars: 0,
218
+ maxChars: 256000,
219
+ percentage: 0
220
+ }
221
+ })
222
+ )
223
+ return
224
+ }
225
+
226
+ let isMounted = true
227
+ import('../api/ai/contextManager')
228
+ .then(({ calculateSessionTokens, MAX_CONTEXT_TOKENS }) => {
229
+ if (!isMounted) return
230
+ const tokens = calculateSessionTokens(
231
+ currentDisplayMessages,
232
+ activeSessionCompact?.summaryBlock || activeSessionCompact?.summary_block || '',
233
+ activeSessionCompact?.lastCompactedMessageId ||
234
+ activeSessionCompact?.last_compacted_message_id ||
235
+ null,
236
+ '',
237
+ activeSessionCompact?.lastCompactedAt || activeSessionCompact?.last_compacted_at || null
238
+ )
239
+ window.dispatchEvent(
240
+ new CustomEvent('context-tracker-updated', {
241
+ detail: {
242
+ sessionId: String(activeSessionId),
243
+ currentTokens: tokens,
244
+ maxTokens: MAX_CONTEXT_TOKENS,
245
+ percentage: Math.min(100, (tokens / MAX_CONTEXT_TOKENS) * 100),
246
+ currentChars: tokens,
247
+ maxChars: MAX_CONTEXT_TOKENS
248
+ }
249
+ })
250
+ )
251
+ })
252
+ .catch(() => {})
253
+
254
+ return () => {
255
+ isMounted = false
256
+ }
257
+ }, [activeSessionId, currentDisplayMessages?.length, activeSessionCompact])
258
+
259
+ const handleSendMessage = async (prompt, sendOptions = {}) => {
260
+ if (!prompt.trim()) return
261
+
262
+ const rawDisplay = sendOptions?.displayPrompt || prompt
263
+ // Auto-update session title if it's default
264
+ const currentSession = sessions.find((s) => String(s.id) === String(activeSessionId))
265
+ let newTitle = currentSession?.title
266
+ if (newTitle === 'Percakapan Baru' && rawDisplay.length > 0) {
267
+ newTitle = rawDisplay.slice(0, 30) + (rawDisplay.length > 30 ? '...' : '')
268
+ await renameSession(activeSessionId, newTitle)
269
+ await loadAllSessions()
270
+ }
271
+
272
+ if (isCurrentLoading && typeof handleIntervention === 'function') {
273
+ handleIntervention(prompt, activeSessionId, { displayPrompt: rawDisplay })
274
+ return
275
+ }
276
+
277
+ const isMain = String(activeSessionId) === '1' || String(activeSessionId) === '1.0'
278
+ const commandOpts = {
279
+ workspaceRoot: currentSession?.workspaceRoot || null,
280
+ displayPrompt: rawDisplay,
281
+ attachedFiles: sendOptions?.attachedFiles,
282
+ ...(!isMain ? { sessionId: activeSessionId, customChatData: activeSessionData } : {})
283
+ }
284
+
285
+ handlePlanningCommand(prompt, false, false, commandOpts)
286
+ }
287
+
288
+ // Real-time live background sync across sessions & trigger quick prompt
289
+ useEffect(() => {
290
+ const handleSessionUpdate = (e) => {
291
+ if (e.detail && String(e.detail.sessionId) === String(activeSessionId)) {
292
+ setActiveSessionData(e.detail.data || [])
293
+ }
294
+ }
295
+
296
+ const handleQuickPrompt = (e) => {
297
+ if (e.detail?.prompt) {
298
+ handleSendMessage(e.detail.prompt)
299
+ }
300
+ }
301
+
302
+ window.addEventListener('session-updated', handleSessionUpdate)
303
+ window.addEventListener('trigger-quick-prompt', handleQuickPrompt)
304
+
305
+ return () => {
306
+ window.removeEventListener('session-updated', handleSessionUpdate)
307
+ window.removeEventListener('trigger-quick-prompt', handleQuickPrompt)
308
+ }
309
+ }, [activeSessionId, handleSendMessage])
310
+
311
+ const lastMessage = currentDisplayMessages[currentDisplayMessages.length - 1]
312
+ const lastMessageContent = lastMessage?.content || ''
313
+ const lastMessageIsThinking = !!lastMessage?.isThinking
314
+ const isAutoScrollEnabledRef = useRef(true)
315
+
316
+ const handleScroll = () => {
317
+ const container = messagesContainerRef.current
318
+ if (!container) return
319
+ const { scrollTop, scrollHeight, clientHeight } = container
320
+ // User is considered at bottom if within 80px
321
+ isAutoScrollEnabledRef.current = scrollHeight - scrollTop - clientHeight < 80
322
+ }
323
+
324
+ const scrollToBottom = (behavior = 'auto') => {
325
+ if (messagesContainerRef.current) {
326
+ messagesContainerRef.current.scrollTo({
327
+ top: messagesContainerRef.current.scrollHeight,
328
+ behavior
329
+ })
330
+ }
331
+ }
332
+
333
+ // Auto scroll to bottom on session change
334
+ useEffect(() => {
335
+ isAutoScrollEnabledRef.current = true
336
+ scrollToBottom('auto')
337
+ }, [activeSessionId])
338
+
339
+ // Direct stick-to-bottom without conflicting timers or layout thrashing
340
+ useEffect(() => {
341
+ if (isAutoScrollEnabledRef.current && messagesContainerRef.current) {
342
+ messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight
343
+ }
344
+ }, [currentDisplayMessages.length, lastMessageContent, lastMessageIsThinking, isCurrentLoading])
345
+
346
+ const handleCreateNewChat = async () => {
347
+ try {
348
+ window.api?.resetAiSession?.().catch(() => {})
349
+ const newSession = await createSession('Percakapan Baru', [])
350
+ await loadAllSessions()
351
+ setActiveSessionId(newSession.id)
352
+ setActiveSessionData([])
353
+ } catch (err) {
354
+ console.error('Failed to create session:', err)
355
+ }
356
+ }
357
+
358
+ const handleDeleteSessionClick = async (e, id) => {
359
+ e.stopPropagation()
360
+ if (id === 1) return
361
+
362
+ const confirmed = await confirm({
363
+ title: 'Hapus Sesi Obrolan',
364
+ message: 'Apakah kamu yakin ingin menghapus sesi percakapan ini secara permanen?',
365
+ confirmText: 'Hapus',
366
+ confirmColor: 'btn-error'
367
+ })
368
+
369
+ if (confirmed?.isConfirmed) {
370
+ await deleteSession(id)
371
+ await loadAllSessions()
372
+ if (String(activeSessionId) === String(id)) {
373
+ setActiveSessionId('1')
374
+ }
375
+ }
376
+ }
377
+
378
+ const handleStartRename = (e, session) => {
379
+ e.stopPropagation()
380
+ setEditingSessionId(session.id)
381
+ setEditingTitle(session.title)
382
+ }
383
+
384
+ const handleSaveRename = async (id) => {
385
+ if (editingTitle.trim()) {
386
+ await renameSession(id, editingTitle.trim())
387
+ await loadAllSessions()
388
+ }
389
+ setEditingSessionId(null)
390
+ }
391
+
392
+ const handleSelectSessionWorkspace = async () => {
393
+ if (window.api && window.api.selectDirectory) {
394
+ const selected = await window.api.selectDirectory()
395
+ if (selected) {
396
+ await setSessionWorkspace(activeSessionId, selected)
397
+ setSessions((prev) =>
398
+ prev.map((s) =>
399
+ String(s.id) === String(activeSessionId) ? { ...s, workspaceRoot: selected } : s
400
+ )
401
+ )
402
+ }
403
+ }
404
+ }
405
+
406
+ const handleStopSession = () => {
407
+ if (handleStop) handleStop(activeSessionId)
408
+ if (window.api && window.api.browserClose) {
409
+ window.api
410
+ .browserClose({
411
+ sessionId: String(activeSessionId) === '1' ? 'main' : `workspace-${activeSessionId}`
412
+ })
413
+ .catch(() => {})
414
+ }
415
+ }
416
+
417
+ const handleManualCompaction = async () => {
418
+ if (!currentDisplayMessages || currentDisplayMessages.length <= 1) return
419
+
420
+ const compactBannerId = `compact-banner-${Date.now()}`
421
+ const bannerMsg = {
422
+ id: compactBannerId,
423
+ role: 'system',
424
+ isCompacting: true,
425
+ compactProgress: 'Merangkum konteks percakapan...'
426
+ }
427
+
428
+ if (String(activeSessionId) === '1') {
429
+ setMainChatData((prev) => [...(prev || []), bannerMsg])
430
+ } else {
431
+ setActiveSessionData((prev) => [...(prev || []), bannerMsg])
432
+ }
433
+
434
+ try {
435
+ const configList = await db.config.toArray()
436
+ const activeConfig = configList?.[0]?.data || configList?.[0] || {}
437
+
438
+ const { executeSessionCompaction } = await import('../api/ai/contextManager')
439
+ const res = await executeSessionCompaction({
440
+ sessionId: String(activeSessionId),
441
+ messages: currentDisplayMessages.filter((m) => !m.isCompacting),
442
+ activeConfig,
443
+ force: true,
444
+ onProgress: (prog) => {
445
+ const updateFn = (prev) =>
446
+ (prev || []).map((m) =>
447
+ m.id === compactBannerId
448
+ ? { ...m, compactProgress: prog?.text || m.compactProgress }
449
+ : m
450
+ )
451
+ if (String(activeSessionId) === '1') {
452
+ setMainChatData(updateFn)
453
+ } else {
454
+ setActiveSessionData(updateFn)
455
+ }
456
+ }
457
+ })
458
+
459
+ if (res?.isCompacted) {
460
+ if (res.compactedMessages) {
461
+ const cleanCompacted = res.compactedMessages.filter((m) => m.id !== compactBannerId)
462
+ if (String(activeSessionId) === '1') {
463
+ setMainChatData(cleanCompacted)
464
+ } else {
465
+ setActiveSessionData(cleanCompacted)
466
+ }
467
+ }
468
+ if (res.lastCompactedMessageId) {
469
+ setLastCompactedMessageId(res.lastCompactedMessageId)
470
+ setActiveSessionCompact({
471
+ summaryBlock: res.newSummaryBlock,
472
+ lastCompactedMessageId: res.lastCompactedMessageId
473
+ })
474
+ }
475
+ window.dispatchEvent(
476
+ new CustomEvent('session-compact-updated', {
477
+ detail: {
478
+ sessionId: String(activeSessionId),
479
+ lastCompactedMessageId: res.lastCompactedMessageId,
480
+ summaryBlock: res.newSummaryBlock
481
+ }
482
+ })
483
+ )
484
+ const currentTok = res.currentTokens ?? res.currentChars ?? 0
485
+ const maxTok = res.maxTokens || 256000
486
+ window.dispatchEvent(
487
+ new CustomEvent('context-tracker-updated', {
488
+ detail: {
489
+ sessionId: String(activeSessionId),
490
+ currentTokens: currentTok,
491
+ maxTokens: maxTok,
492
+ currentChars: currentTok,
493
+ maxChars: maxTok,
494
+ percentage: Math.min(100, (currentTok / maxTok) * 100),
495
+ lastCompactedAt: Date.now()
496
+ }
497
+ })
498
+ )
499
+ }
500
+ } catch (e) {
501
+ console.error('[ChatStudio] Manual compaction error:', e)
502
+ } finally {
503
+ if (String(activeSessionId) === '1') {
504
+ setMainChatData((prev) => (prev || []).filter((m) => m.id !== compactBannerId))
505
+ } else {
506
+ setActiveSessionData((prev) => (prev || []).filter((m) => m.id !== compactBannerId))
507
+ }
508
+ }
509
+ }
510
+
511
+ useEffect(() => {
512
+ const handleReqCompact = () => handleManualCompaction()
513
+ window.addEventListener('request-manual-compaction', handleReqCompact)
514
+ return () => window.removeEventListener('request-manual-compaction', handleReqCompact)
515
+ }, [activeSessionId, currentDisplayMessages])
516
+
517
+ const handleClose = () => {
518
+ if (typeof onClose === 'function') {
519
+ onClose()
520
+ } else {
521
+ navigate('/')
522
+ }
523
+ }
524
+
525
+ // Jika dipanggil via route (tanpa prop isOpen), default ke true
526
+ if (isOpen === false) return null
527
+
528
+ const filteredSessions = sessions.filter((s) =>
529
+ (s.title || '').toLowerCase().includes(searchQuery.toLowerCase())
530
+ )
531
+
532
+ const activeSessionObj = sessions.find((s) => String(s.id) === String(activeSessionId)) || {
533
+ id: '1',
534
+ title: 'Main Thread'
535
+ }
536
+ const activeSessionWorkspace = activeSessionObj?.workspaceRoot || null
537
+
538
+ return (
539
+ <>
540
+ <div className="fixed inset-0 z-[80] w-screen h-screen bg-base-300 flex overflow-hidden animate-[response-fade-in_0.2s_ease-out_forwards]">
541
+ {loading ? (
542
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 relative">
543
+ <button
544
+ type="button"
545
+ onClick={handleClose}
546
+ className="absolute top-4 left-4 btn btn-ghost btn-sm btn-circle text-white/70 hover:text-white pointer-events-auto"
547
+ title="Tutup Studio (Esc)"
548
+ >
549
+ <ArrowLeft className="w-5 h-5" />
550
+ </button>
551
+ <span className="loading loading-spinner loading-lg text-primary" />
552
+ <p className="font-mono text-xs text-base-content/50 tracking-widest uppercase">
553
+ Memuat Chat Studio...
554
+ </p>
555
+ </div>
556
+ ) : (
557
+ <>
558
+ <div className="w-80 border-r border-white/10 bg-base-200/50 flex flex-col h-full shrink-0">
559
+ {/* Sidebar Header */}
560
+ <div className="p-4 border-b border-white/10 space-y-3 z-30 relative select-none">
561
+ <div className="flex items-center justify-between pointer-events-auto">
562
+ <div className="flex items-center gap-2 font-bold text-sm tracking-wide text-white">
563
+ <button
564
+ type="button"
565
+ onClick={(e) => {
566
+ e.stopPropagation()
567
+ handleClose()
568
+ }}
569
+ className="btn btn-ghost btn-sm btn-circle text-white/70 hover:text-white mr-1 cursor-pointer shrink-0 pointer-events-auto"
570
+ title="Tutup Studio (Esc)"
571
+ >
572
+ <ArrowLeft className="w-5 h-5" />
573
+ </button>
574
+ <span className="truncate">Chat Studio</span>
575
+ </div>
576
+ <div className="flex items-center gap-1 pointer-events-auto">
577
+ <button
578
+ type="button"
579
+ onClick={handleCreateNewChat}
580
+ className="btn btn-xs btn-primary rounded-lg gap-1 font-medium shadow-md shadow-primary/20 cursor-pointer pointer-events-auto"
581
+ >
582
+ <Plus className="w-3.5 h-3.5" />
583
+ Sesi Baru
584
+ </button>
585
+ </div>
586
+ </div>
587
+
588
+ {/* Search Input */}
589
+ <div className="relative pointer-events-auto">
590
+ <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-white/40" />
591
+ <input
592
+ type="text"
593
+ placeholder="Cari obrolan..."
594
+ value={searchQuery}
595
+ onChange={(e) => setSearchQuery(e.target.value)}
596
+ className="input input-sm bg-base-300 border-white/10 pl-9 w-full rounded-xl text-xs text-white placeholder:text-white/30 focus:border-primary/50"
597
+ />
598
+ </div>
599
+ </div>
600
+
601
+ {/* Sessions Scroll List */}
602
+ <div className="flex-1 overflow-y-auto p-2.5 space-y-1 custom-scrollbar">
603
+ {/* PINNED MAIN THREAD */}
604
+ <div className="px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-white/40 flex items-center gap-1">
605
+ <span>Sesi Utama</span>
606
+ </div>
607
+
608
+ <div
609
+ role="button"
610
+ tabIndex={0}
611
+ onClick={() => setActiveSessionId('1')}
612
+ onKeyDown={(e) => {
613
+ if (e.key === 'Enter' || e.key === ' ') setActiveSessionId('1')
614
+ }}
615
+ className={`w-full p-2.5 rounded-xl text-left transition-all flex items-center justify-between group/item cursor-pointer ${
616
+ String(activeSessionId) === '1'
617
+ ? 'bg-primary/20 border border-primary/40 text-white shadow-sm'
618
+ : 'hover:bg-white/5 text-white/70 hover:text-white border border-transparent'
619
+ }`}
620
+ >
621
+ <div className="flex items-center gap-2.5 min-w-0">
622
+ <div
623
+ className={`w-2 h-2 rounded-full shrink-0 ${
624
+ runningSessionIds.map(String).includes('1') ||
625
+ (!runningSessionIds.length && (isMainLoading || isAgentBusy))
626
+ ? 'bg-warning animate-ping'
627
+ : 'bg-primary shadow-[0_0_8px_var(--color-primary)]'
628
+ }`}
629
+ />
630
+ <div className="min-w-0">
631
+ <h4 className="text-xs font-semibold truncate">Main Thread</h4>
632
+ </div>
633
+ </div>
634
+ </div>
635
+
636
+ <div className="my-2 border-t border-white/5" />
637
+
638
+ {/* WORKSPACE SESSIONS */}
639
+ <div className="px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-white/40">
640
+ Semua Sesi
641
+ </div>
642
+
643
+ {filteredSessions
644
+ .filter(
645
+ (s) =>
646
+ String(s.id) !== '1' && String(s.id) !== '1.0' && s.title !== 'Main Thread'
647
+ )
648
+ .map((s) => {
649
+ const isActive = String(activeSessionId) === String(s.id)
650
+ const isEditing = String(editingSessionId) === String(s.id)
651
+ const isThisSessionRunning = runningSessionIds
652
+ .map(String)
653
+ .includes(String(s.id))
654
+
655
+ return (
656
+ <div
657
+ key={s.id}
658
+ onClick={() => setActiveSessionId(s.id)}
659
+ className={`w-full p-2.5 rounded-xl text-left transition-all flex items-center justify-between group/session cursor-pointer ${
660
+ isActive
661
+ ? 'bg-white/10 border border-white/20 text-white shadow-sm'
662
+ : 'hover:bg-white/5 text-white/70 hover:text-white border border-transparent'
663
+ }`}
664
+ >
665
+ <div className="flex items-center gap-2 min-w-0 flex-1 pr-2">
666
+ <div
667
+ className={`w-2 h-2 rounded-full shrink-0 ${
668
+ isThisSessionRunning
669
+ ? 'bg-warning animate-ping'
670
+ : isActive
671
+ ? 'bg-primary shadow-[0_0_6px_var(--color-primary)]'
672
+ : 'bg-white/20'
673
+ }`}
674
+ />
675
+ <MessageSquare className="w-3.5 h-3.5 opacity-50 shrink-0" />
676
+ {isEditing ? (
677
+ <input
678
+ type="text"
679
+ value={editingTitle}
680
+ onChange={(e) => setEditingTitle(e.target.value)}
681
+ onKeyDown={(e) => {
682
+ if (e.key === 'Enter') handleSaveRename(s.id)
683
+ if (e.key === 'Escape') setEditingSessionId(null)
684
+ }}
685
+ autoFocus
686
+ className="input input-xs bg-base-300 border-primary/50 text-xs text-white p-1 h-6 w-full rounded"
687
+ onClick={(e) => e.stopPropagation()}
688
+ />
689
+ ) : (
690
+ <div className="min-w-0 flex-1">
691
+ <h4 className="text-xs font-medium truncate">
692
+ {s.title || 'Percakapan'}
693
+ </h4>
694
+ <p className="text-[10px] opacity-40">
695
+ {s.timestamp
696
+ ? new Date(s.timestamp).toLocaleDateString('id-ID', {
697
+ month: 'short',
698
+ day: 'numeric'
699
+ })
700
+ : ''}
701
+ </p>
702
+ </div>
703
+ )}
704
+ </div>
705
+
706
+ {/* Action buttons on hover */}
707
+ <div className="flex items-center gap-1 opacity-0 group-hover/session:opacity-100 transition-opacity">
708
+ {isEditing ? (
709
+ <button
710
+ onClick={(e) => {
711
+ e.stopPropagation()
712
+ handleSaveRename(s.id)
713
+ }}
714
+ className="btn btn-ghost btn-xs p-1 text-success hover:bg-success/20"
715
+ >
716
+ <Check className="w-3 h-3" />
717
+ </button>
718
+ ) : (
719
+ <>
720
+ <button
721
+ onClick={(e) => handleStartRename(e, s)}
722
+ className="btn btn-ghost btn-xs p-1 text-white/40 hover:text-white"
723
+ title="Ubah judul sesi"
724
+ >
725
+ <Edit2 className="w-3 h-3" />
726
+ </button>
727
+ <button
728
+ onClick={(e) => handleDeleteSessionClick(e, s.id)}
729
+ className="btn btn-ghost btn-xs p-1 text-white/40 hover:text-error"
730
+ title="Hapus sesi"
731
+ >
732
+ <Trash2 className="w-3 h-3" />
733
+ </button>
734
+ </>
735
+ )}
736
+ </div>
737
+ </div>
738
+ )
739
+ })}
740
+
741
+ {filteredSessions.filter((s) => s.id !== 1).length === 0 && (
742
+ <div className="text-center py-6 text-xs text-white/30">
743
+ Belum ada sesi workspace lain.
744
+ </div>
745
+ )}
746
+ </div>
747
+ </div>
748
+
749
+ {/* === RIGHT MAIN: BUBBLE CHAT AREA === */}
750
+ <div className="flex-1 flex flex-col h-full bg-base-300/60 relative min-w-0 overflow-hidden">
751
+ {/* Header */}
752
+ <div className="h-14 px-6 border-b border-white/10 flex items-center justify-between bg-base-300/80 backdrop-blur-md shrink-0 z-30 relative select-none">
753
+ <div className="flex flex-col min-w-0 pr-4">
754
+ <div className="flex items-center gap-2">
755
+ <h3 className="text-sm font-bold text-white truncate max-w-md">
756
+ {activeSessionObj.title || 'Percakapan'}
757
+ </h3>
758
+ {activeSessionWorkspace && (
759
+ <span
760
+ className="badge badge-xs bg-primary/10 text-primary border-primary/30 font-mono text-[9px] px-2 py-0.5 max-w-65 truncate inline-flex items-center gap-1"
761
+ title={`Workspace: ${activeSessionWorkspace}`}
762
+ >
763
+ <Folder className="w-2.5 h-2.5 shrink-0" />
764
+ <span className="truncate">
765
+ {activeSessionWorkspace.split(/[\\/]/).pop()}
766
+ </span>
767
+ </span>
768
+ )}
769
+ </div>
770
+ <span className="text-[10px] text-white/40">
771
+ {currentDisplayMessages.length} pesan terdaftar
772
+ </span>
773
+ </div>
774
+
775
+ <div className="flex items-center gap-2 pointer-events-auto mr-28"></div>
776
+ </div>
777
+
778
+ {/* Messages Stream Container */}
779
+ <div
780
+ ref={messagesContainerRef}
781
+ onScroll={handleScroll}
782
+ className="flex-1 overflow-y-auto overflow-x-hidden px-4 md:px-8 py-6 custom-scrollbar min-h-0"
783
+ >
784
+ <div className="max-w-6xl mx-auto w-full min-h-full flex flex-col space-y-8">
785
+ {currentDisplayMessages.length > visibleMessageCount && (
786
+ <div className="flex justify-center py-2">
787
+ <button
788
+ type="button"
789
+ onClick={() => setVisibleMessageCount((prev) => prev + 30)}
790
+ className="btn btn-xs btn-ghost text-[11px] text-white/50 hover:text-white border border-white/10 rounded-full px-4 normal-case cursor-pointer"
791
+ >
792
+ Muat pesan sebelumnya ({currentDisplayMessages.length - visibleMessageCount}{' '}
793
+ pesan lagi)
794
+ </button>
795
+ </div>
796
+ )}
797
+
798
+ {currentDisplayMessages.length === 0 ? (
799
+ <div className="h-full flex flex-col items-center justify-center text-center p-8 text-white/40 space-y-4 my-auto">
800
+ <img src="/icon-256.png" alt="icon" className="size-30" />
801
+ <div className="max-w-lg space-y-1">
802
+ <h4 className="text-xl font-bold text-white">
803
+ Selamat Datang di Mark Agent
804
+ </h4>
805
+ <p className="text-lg text-white/50">
806
+ Tanyakan apapun, analisis kode, atau diskusikan ide riset bersama Mark.
807
+ </p>
808
+ </div>
809
+ </div>
810
+ ) : (
811
+ (() => {
812
+ const hasActivePlan = (currentDisplayMessages || []).some(
813
+ (m) => m.isPlanSteps && m.taskStatus === 'running'
814
+ )
815
+ const activeThinkingMsg =
816
+ [...(currentDisplayMessages || [])].reverse().find((m) => m.isThinking) ||
817
+ null
818
+
819
+ return currentDisplayMessages.slice(-visibleMessageCount).map((msg, idx) => {
820
+ if (hasActivePlan && msg.isThinking && !msg.isPlanSteps) {
821
+ return null
822
+ }
823
+ const { cleanContent } = filterMessagePrefixes(msg.content)
824
+ return (
825
+ <ChatList
826
+ key={
827
+ msg.id
828
+ ? `${msg.id}-${idx}`
829
+ : `${msg.created_at || msg.timestamp || 'msg'}-${idx}`
830
+ }
831
+ id={msg.id || msg.timestamp || msg.created_at}
832
+ lastCompactedMessageId={lastCompactedMessageId}
833
+ isCompacting={msg.isCompacting}
834
+ compactProgress={msg.compactProgress}
835
+ role={msg.role}
836
+ content={cleanContent}
837
+ reasoning={msg.reasoning}
838
+ isThinking={msg.isThinking}
839
+ isSearching={msg.isSearching}
840
+ isSummarizing={msg.isSummarizing}
841
+ isSearchingMusic={msg.isSearchingMusic}
842
+ sources={msg.sources}
843
+ executedTools={msg.executedTools}
844
+ isMemorySaved={msg.isMemorySaved}
845
+ isMemoryUpdated={msg.isMemoryUpdated}
846
+ isMemoryDeleted={msg.isMemoryDeleted}
847
+ timestamp={msg.timestamp}
848
+ mood={msg.mood}
849
+ source={msg.source}
850
+ sender={msg.sender}
851
+ isPlanSteps={msg.isPlanSteps}
852
+ plan={msg.plan}
853
+ currentStep={msg.currentStep}
854
+ taskId={msg.taskId}
855
+ taskTitle={msg.taskTitle}
856
+ taskObjective={msg.taskObjective}
857
+ taskStatus={msg.taskStatus}
858
+ artifactRoot={msg.artifactRoot}
859
+ activeLiveTools={
860
+ hasActivePlan && msg.isPlanSteps
861
+ ? activeThinkingMsg?.executedTools || null
862
+ : null
863
+ }
864
+ activeThinkingContent={
865
+ hasActivePlan && msg.isPlanSteps
866
+ ? (activeThinkingMsg?.reasoning
867
+ ? activeThinkingMsg.reasoning
868
+ .split('\n')
869
+ .map((s) => s.trim())
870
+ .filter(Boolean)
871
+ .pop()
872
+ : null) ||
873
+ activeThinkingMsg?.content ||
874
+ null
875
+ : null
876
+ }
877
+ onStop={handleStopSession}
878
+ isApproval={msg.isApproval}
879
+ approvalId={msg.approvalId}
880
+ approvalTool={msg.tool}
881
+ approvalQuery={msg.query}
882
+ approvalStatus={msg.status}
883
+ />
884
+ )
885
+ })
886
+ })()
887
+ )}
888
+ <div ref={messagesEndRef} className="h-2" />
889
+ </div>
890
+ </div>
891
+
892
+ {/* Bottom Input Area */}
893
+ <div className="p-3 border-t border-white/10 bg-base-200/40 shrink-0">
894
+ <div className="max-w-6xl mx-auto w-full h-full">
895
+ <InputBar
896
+ sessionId={String(activeSessionId)}
897
+ inline={true}
898
+ onSubmit={handleSendMessage}
899
+ isLoading={isCurrentLoading}
900
+ isRecording={isRecording}
901
+ isProcessing={isProcessing}
902
+ audioIntensity={audioIntensity}
903
+ onStartRecord={startRecording}
904
+ onStopRecord={stopRecording}
905
+ onStop={handleStopSession}
906
+ source={inputSource || 'pc'}
907
+ workspaceRoot={activeSessionWorkspace}
908
+ onSelectWorkspace={handleSelectSessionWorkspace}
909
+ onManualCompact={handleManualCompaction}
910
+ />
911
+ </div>
912
+ </div>
913
+ </div>
914
+ </>
915
+ )}
916
+ </div>
917
+
918
+ <ModalComponent />
919
+ </>
920
+ )
921
+ }
922
+ export default ChatStudio