@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,1346 @@
1
+ /* eslint-disable react/prop-types */
2
+ import { useRef, useEffect, useState, useMemo, useCallback, memo } from 'react'
3
+ import {
4
+ FaMicrophone,
5
+ FaStop,
6
+ FaArrowUp,
7
+ FaSmile,
8
+ FaPaperclip,
9
+ FaTimes,
10
+ FaFileAlt,
11
+ FaFilePdf,
12
+ FaFileCode,
13
+ FaFileImage,
14
+ FaLock,
15
+ FaFolder
16
+ } from 'react-icons/fa'
17
+ import { Zap, Folder, Wrench, ChevronLeft } from 'lucide-react'
18
+ import ConfirmModal from './ConfirmModal'
19
+ import { NATIVE_SKILLS } from './native-skills'
20
+ import { calculateSessionTokens, MAX_CONTEXT_TOKENS } from '../../api/ai/contextManager'
21
+ import {
22
+ getChatData,
23
+ getSessionCompact,
24
+ getSessionAutoMode,
25
+ setSessionAutoMode
26
+ } from '../../api/db'
27
+
28
+ const EMOJIS = [
29
+ '😂',
30
+ '🤣',
31
+ '😅',
32
+ '🗿',
33
+ '🙏',
34
+ '🔥',
35
+ '🚀',
36
+ '💀',
37
+ '😎',
38
+ '🤔',
39
+ '😭',
40
+ '❤️',
41
+ '👍',
42
+ '✨',
43
+ '👀',
44
+ '💯'
45
+ ]
46
+
47
+ const formatFileSize = (bytes) => {
48
+ if (!bytes) return ''
49
+ if (bytes < 1024) return bytes + ' B'
50
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
51
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
52
+ }
53
+
54
+ const getFileIcon = (fileName = '') => {
55
+ const ext = fileName.split('.').pop().toLowerCase()
56
+ if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext))
57
+ return <FaFileImage className="text-primary" />
58
+ if (['pdf'].includes(ext)) return <FaFilePdf className="text-error" />
59
+ if (['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'json', 'py', 'cpp', 'cs'].includes(ext))
60
+ return <FaFileCode className="text-info" />
61
+ return <FaFileAlt className="text-primary" />
62
+ }
63
+
64
+ const InputBar = ({
65
+ sessionId = '1',
66
+ onSubmit,
67
+ isLoading,
68
+ isRecording,
69
+ isProcessing,
70
+ audioIntensity = 0,
71
+ onStartRecord,
72
+ onStopRecord,
73
+ onStop,
74
+ source = 'pc',
75
+ inline = false,
76
+ className = '',
77
+ workspaceRoot = null,
78
+ onSelectWorkspace = null,
79
+ onManualCompact = null
80
+ }) => {
81
+ void source
82
+ const inputRef = useRef(null)
83
+ const fileInputRef = useRef(null)
84
+ const contextPopoverRef = useRef(null)
85
+ const gaugeRef = useRef(null)
86
+ const mentionListRef = useRef(null)
87
+ const skillListRef = useRef(null)
88
+ const [inputText, setInputText] = useState('')
89
+ const [showEmojiPicker, setShowEmojiPicker] = useState(false)
90
+ const [showAbortConfirm, setShowAbortConfirm] = useState(false)
91
+ const [showContextPopover, setShowContextPopover] = useState(false)
92
+ const [attachedFiles, setAttachedFiles] = useState([])
93
+ const [isDragging, setIsDragging] = useState(false)
94
+ const lastPromptRef = useRef('')
95
+ const isPastingRef = useRef(false)
96
+
97
+ const [skills, setSkills] = useState([])
98
+ const [filteredSkills, setFilteredSkills] = useState([])
99
+ const [showSkillList, setShowSkillList] = useState(false)
100
+ const [selectedSkillIndex, setSelectedSkillIndex] = useState(0)
101
+
102
+ // Mention (@) Autocomplete States
103
+ const [showMentionList, setShowMentionList] = useState(false)
104
+ const [mentionCategory, setMentionCategory] = useState('all') // 'all' | 'files' | 'tools'
105
+ const [mentionQuery, setMentionQuery] = useState('')
106
+ const [mentionCursorIndex, setMentionCursorIndex] = useState(0)
107
+ const [selectedMentionIndex, setSelectedMentionIndex] = useState(0)
108
+ const [workspaceFiles, setWorkspaceFiles] = useState([])
109
+ const [isLoadingFiles, setIsLoadingFiles] = useState(false)
110
+ const [toolGroups, setToolGroups] = useState([])
111
+
112
+ const loadToolGroups = useCallback(async () => {
113
+ if (window.api && window.api.getGroupTools) {
114
+ try {
115
+ const data = await window.api.getGroupTools()
116
+ const schema = data?.schema || {}
117
+ const names = data?.names || Object.keys(schema)
118
+ const list = names.map((key) => {
119
+ const desc = schema[key]?.description || ''
120
+ const label = key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
121
+ return {
122
+ name: key,
123
+ label,
124
+ description: desc
125
+ }
126
+ })
127
+ setToolGroups(list)
128
+ return list
129
+ } catch (err) {
130
+ console.error('[InputBar] Failed to load tool groups:', err)
131
+ }
132
+ }
133
+ return []
134
+ }, [])
135
+
136
+ useEffect(() => {
137
+ loadToolGroups()
138
+ }, [loadToolGroups])
139
+
140
+ const [contextTracker, setContextTracker] = useState({
141
+ currentTokens: 0,
142
+ maxTokens: MAX_CONTEXT_TOKENS,
143
+ percentage: 0,
144
+ lastCompactedAt: null
145
+ })
146
+
147
+ const [isAutoMode, setIsAutoMode] = useState(false)
148
+
149
+ useEffect(() => {
150
+ let isMounted = true
151
+ getSessionAutoMode(sessionId).then((val) => {
152
+ if (isMounted) setIsAutoMode(Boolean(val))
153
+ })
154
+
155
+ const handleAutoModeUpdate = (e) => {
156
+ if (String(e.detail?.sessionId) === String(sessionId)) {
157
+ setIsAutoMode(Boolean(e.detail?.isAutoMode))
158
+ }
159
+ }
160
+
161
+ window.addEventListener('session-auto-mode-updated', handleAutoModeUpdate)
162
+ return () => {
163
+ isMounted = false
164
+ window.removeEventListener('session-auto-mode-updated', handleAutoModeUpdate)
165
+ }
166
+ }, [sessionId])
167
+
168
+ const handleToggleAutoMode = async () => {
169
+ const next = !isAutoMode
170
+ setIsAutoMode(next)
171
+ await setSessionAutoMode(sessionId, next)
172
+ }
173
+
174
+ useEffect(() => {
175
+ const handleClickOutside = (e) => {
176
+ if (
177
+ contextPopoverRef.current &&
178
+ !contextPopoverRef.current.contains(e.target) &&
179
+ (!gaugeRef.current || !gaugeRef.current.contains(e.target))
180
+ ) {
181
+ setShowContextPopover(false)
182
+ }
183
+ }
184
+ if (showContextPopover) {
185
+ document.addEventListener('mousedown', handleClickOutside)
186
+ }
187
+ return () => {
188
+ document.removeEventListener('mousedown', handleClickOutside)
189
+ }
190
+ }, [showContextPopover])
191
+
192
+ // Muat status konteks awal sesi saat mount atau saat sessionId berganti
193
+ useEffect(() => {
194
+ let isCancelled = false
195
+ const loadInitialContext = async () => {
196
+ try {
197
+ const [messages, compact] = await Promise.all([
198
+ getChatData(sessionId),
199
+ getSessionCompact(String(sessionId))
200
+ ])
201
+ if (isCancelled) return
202
+ const tokens = calculateSessionTokens(
203
+ messages || [],
204
+ compact?.summaryBlock || compact?.summary_block || '',
205
+ compact?.lastCompactedMessageId || compact?.last_compacted_message_id || null,
206
+ '',
207
+ compact?.lastCompactedAt || compact?.last_compacted_at || null
208
+ )
209
+ setContextTracker({
210
+ currentTokens: tokens,
211
+ maxTokens: MAX_CONTEXT_TOKENS,
212
+ percentage: Math.min(100, (tokens / MAX_CONTEXT_TOKENS) * 100),
213
+ lastCompactedAt: compact?.lastCompactedAt || null
214
+ })
215
+ } catch {
216
+ // ignore
217
+ }
218
+ }
219
+ loadInitialContext()
220
+ return () => {
221
+ isCancelled = true
222
+ }
223
+ }, [sessionId])
224
+
225
+ useEffect(() => {
226
+ const handleTrackerUpdate = (e) => {
227
+ if (e.detail) {
228
+ if (e.detail.sessionId && String(e.detail.sessionId) !== String(sessionId)) {
229
+ return
230
+ }
231
+ const tokens = Number(e.detail.currentTokens ?? e.detail.currentChars ?? 0)
232
+ const maxTokens = Number(e.detail.maxTokens ?? e.detail.maxChars ?? MAX_CONTEXT_TOKENS)
233
+ setContextTracker({
234
+ currentTokens: tokens,
235
+ maxTokens,
236
+ percentage: Number(
237
+ e.detail.percentage !== undefined
238
+ ? e.detail.percentage
239
+ : maxTokens > 0
240
+ ? Math.min(100, (tokens / maxTokens) * 100)
241
+ : 0
242
+ ),
243
+ lastCompactedAt: e.detail.lastCompactedAt || null
244
+ })
245
+ }
246
+ }
247
+
248
+ window.addEventListener('context-tracker-updated', handleTrackerUpdate)
249
+ return () => {
250
+ window.removeEventListener('context-tracker-updated', handleTrackerUpdate)
251
+ }
252
+ }, [sessionId])
253
+
254
+ const reloadSkills = async () => {
255
+ if (window.api && window.api.getSkills) {
256
+ try {
257
+ const loadedSkills = await window.api.getSkills()
258
+ const nativeSkillList = NATIVE_SKILLS.map((s) => ({
259
+ name: s.name,
260
+ description: s.description
261
+ }))
262
+ const merged = [...nativeSkillList, ...(loadedSkills || [])]
263
+ setSkills(merged)
264
+ return merged
265
+ } catch (err) {
266
+ console.error('[InputBar] Failed to reload skills:', err)
267
+ }
268
+ }
269
+ return []
270
+ }
271
+
272
+ useEffect(() => {
273
+ reloadSkills()
274
+ if (window.api && window.api.onSkillsUpdated) {
275
+ const unsub = window.api.onSkillsUpdated(() => {
276
+ reloadSkills()
277
+ })
278
+ return () => {
279
+ if (typeof unsub === 'function') unsub()
280
+ }
281
+ }
282
+ }, [])
283
+
284
+ useEffect(() => {
285
+ if (!isLoading && inputRef.current) {
286
+ setTimeout(() => {
287
+ if (inputRef.current) inputRef.current.focus()
288
+ }, 50)
289
+ }
290
+ }, [isLoading])
291
+
292
+ // Auto-scroll item terpilih ke viewport saat navigasi keyboard
293
+ useEffect(() => {
294
+ if (showMentionList && mentionListRef.current) {
295
+ const activeEl = mentionListRef.current.children[selectedMentionIndex]
296
+ if (activeEl && typeof activeEl.scrollIntoView === 'function') {
297
+ activeEl.scrollIntoView({ block: 'nearest' })
298
+ }
299
+ }
300
+ }, [selectedMentionIndex, showMentionList])
301
+
302
+ useEffect(() => {
303
+ if (showSkillList && skillListRef.current) {
304
+ const activeEl = skillListRef.current.children[selectedSkillIndex]
305
+ if (activeEl && typeof activeEl.scrollIntoView === 'function') {
306
+ activeEl.scrollIntoView({ block: 'nearest' })
307
+ }
308
+ }
309
+ }, [selectedSkillIndex, showSkillList])
310
+
311
+ const handleFileChange = (e) => {
312
+ const files = Array.from(e.target.files || [])
313
+ addFiles(files)
314
+ if (fileInputRef.current) fileInputRef.current.value = ''
315
+ }
316
+
317
+ const handlePaperclipClick = async () => {
318
+ if (window.api && window.api.showOpenDialog) {
319
+ try {
320
+ const filePaths = await window.api.showOpenDialog()
321
+ if (filePaths && filePaths.length > 0) {
322
+ const dialogFiles = filePaths.map((p) => ({
323
+ name: p.split(/[/\\]/).pop(),
324
+ path: p,
325
+ size: 0,
326
+ type: ''
327
+ }))
328
+ setAttachedFiles((prev) => {
329
+ const existingPaths = new Set(prev.map((item) => item.path))
330
+ const unique = dialogFiles.filter((item) => !existingPaths.has(item.path))
331
+ return [...prev, ...unique]
332
+ })
333
+ return
334
+ }
335
+ } catch (err) {
336
+ console.error('[InputBar] Open dialog error:', err)
337
+ }
338
+ }
339
+ fileInputRef.current?.click()
340
+ }
341
+
342
+ const handlePaste = async (e) => {
343
+ if (isPastingRef.current) return
344
+ const clipboardData = e.clipboardData || window.clipboardData
345
+ if (!clipboardData) return
346
+
347
+ let imageFiles = []
348
+
349
+ // 1. Ambil file bawaan dari clipboardData.files (sudah ter-deduplikasi oleh browser)
350
+ if (clipboardData.files && clipboardData.files.length > 0) {
351
+ imageFiles = Array.from(clipboardData.files).filter(
352
+ (f) => f.type && f.type.startsWith('image/')
353
+ )
354
+ }
355
+
356
+ // 2. Fallback jika files kosong, cek clipboardData.items (hanya ambil file unik pertama)
357
+ if (imageFiles.length === 0 && clipboardData.items && clipboardData.items.length > 0) {
358
+ for (let i = 0; i < clipboardData.items.length; i++) {
359
+ const item = clipboardData.items[i]
360
+ if (item.type && item.type.startsWith('image/')) {
361
+ const file = item.getAsFile()
362
+ if (file) {
363
+ const ext = item.type.split('/')[1] || 'png'
364
+ const namedFile = new File([file], `screenshot-${Date.now()}.${ext}`, {
365
+ type: item.type
366
+ })
367
+ imageFiles.push(namedFile)
368
+ break
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ if (imageFiles.length > 0) {
375
+ e.preventDefault()
376
+ e.stopPropagation()
377
+ isPastingRef.current = true
378
+ try {
379
+ await addFiles(imageFiles)
380
+ } finally {
381
+ setTimeout(() => {
382
+ isPastingRef.current = false
383
+ }, 300)
384
+ }
385
+ }
386
+ }
387
+
388
+ const addFiles = async (newFiles) => {
389
+ const parsedFiles = await Promise.all(
390
+ newFiles.map(async (f) => {
391
+ let resolvedPath = ''
392
+ let previewUrl = null
393
+
394
+ // Baca Base64 untuk preview jika file gambar
395
+ if (f.type && f.type.startsWith('image/')) {
396
+ try {
397
+ previewUrl = await new Promise((res) => {
398
+ const reader = new FileReader()
399
+ reader.onload = () => res(reader.result)
400
+ reader.onerror = () => res(null)
401
+ reader.readAsDataURL(f)
402
+ })
403
+ } catch {
404
+ // ignore preview error
405
+ }
406
+ }
407
+
408
+ if (window.api && window.api.getPathForFile) {
409
+ try {
410
+ resolvedPath = window.api.getPathForFile(f)
411
+ } catch (e) {
412
+ console.error('[InputBar] getPathForFile error:', e)
413
+ }
414
+ }
415
+
416
+ const isRealDiskPath =
417
+ resolvedPath &&
418
+ resolvedPath !== f.name &&
419
+ (resolvedPath.includes('/') || resolvedPath.includes('\\'))
420
+
421
+ if (
422
+ !isRealDiskPath &&
423
+ f.path &&
424
+ f.path !== f.name &&
425
+ (f.path.includes('/') || f.path.includes('\\'))
426
+ ) {
427
+ resolvedPath = f.path
428
+ }
429
+
430
+ // Jika file belum memiliki path disk asli (misal pasted image atau drag-drop browser)
431
+ if (
432
+ !resolvedPath ||
433
+ resolvedPath === f.name ||
434
+ (!resolvedPath.includes('/') && !resolvedPath.includes('\\'))
435
+ ) {
436
+ try {
437
+ if (window.api?.saveTempFile && previewUrl) {
438
+ const tempPath = await window.api.saveTempFile(previewUrl, f.name)
439
+ if (tempPath) resolvedPath = tempPath
440
+ } else {
441
+ const buffer = await f.arrayBuffer()
442
+ if (buffer && buffer.byteLength > 0 && window.api?.saveTempFile) {
443
+ const tempPath = await window.api.saveTempFile(buffer, f.name)
444
+ if (tempPath) resolvedPath = tempPath
445
+ }
446
+ }
447
+ } catch (err) {
448
+ console.error('[InputBar] Failed to save dragged/pasted file to temp:', err)
449
+ }
450
+ }
451
+
452
+ if (!resolvedPath && previewUrl) {
453
+ resolvedPath = previewUrl
454
+ }
455
+ if (!resolvedPath) resolvedPath = f.name
456
+
457
+ return {
458
+ name: f.name,
459
+ path: resolvedPath,
460
+ size: f.size,
461
+ type: f.type,
462
+ previewUrl: previewUrl || (resolvedPath.startsWith('data:image/') ? resolvedPath : null)
463
+ }
464
+ })
465
+ )
466
+
467
+ setAttachedFiles((prev) => {
468
+ const existingPaths = new Set(prev.map((p) => p.path))
469
+ const unique = parsedFiles.filter((p) => !existingPaths.has(p.path))
470
+ return [...prev, ...unique]
471
+ })
472
+
473
+ setTimeout(() => {
474
+ if (inputRef.current) inputRef.current.focus()
475
+ }, 50)
476
+ }
477
+
478
+ const removeFile = (indexToRemove) => {
479
+ setAttachedFiles((prev) => prev.filter((_, idx) => idx !== indexToRemove))
480
+ }
481
+
482
+ const handleDragOver = (e) => {
483
+ e.preventDefault()
484
+ e.stopPropagation()
485
+ if (!isDragging) setIsDragging(true)
486
+ }
487
+
488
+ const handleDragLeave = (e) => {
489
+ e.preventDefault()
490
+ e.stopPropagation()
491
+ setIsDragging(false)
492
+ }
493
+
494
+ const handleDrop = (e) => {
495
+ e.preventDefault()
496
+ e.stopPropagation()
497
+ setIsDragging(false)
498
+
499
+ if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
500
+ const files = Array.from(e.dataTransfer.files)
501
+ addFiles(files)
502
+ }
503
+ }
504
+
505
+ const handleFormSubmit = async () => {
506
+ let finalPrompt = inputText
507
+ let userText = inputText
508
+ const skillMatches = inputText.match(/(?:\s|^)\/([a-zA-Z0-9_-]+)/g)
509
+
510
+ if (skillMatches && skillMatches.length > 0 && window.api && window.api.readSkill) {
511
+ let combinedSkillsContent = ''
512
+ const loadedSkills = []
513
+
514
+ for (const match of skillMatches) {
515
+ const skillName = match.trim().substring(1) // Hilangkan spasi dan '/'
516
+
517
+ // INTERCEPT BUILT-IN SKILLS
518
+ const nativeSkill = NATIVE_SKILLS.find(
519
+ (s) => s.name.toLowerCase() === skillName.toLowerCase()
520
+ )
521
+ if (nativeSkill) {
522
+ combinedSkillsContent += `\n\n--- SKILL BAWAAN: ${skillName.toUpperCase()} ---\n${nativeSkill.content}`
523
+ loadedSkills.push(skillName)
524
+ userText = userText.replace(match, '')
525
+ continue
526
+ }
527
+
528
+ try {
529
+ const skillData = await window.api.readSkill(skillName)
530
+ if (skillData) {
531
+ // Support both old string format and new object format
532
+ const content = typeof skillData === 'string' ? skillData : skillData.content
533
+ const basePath =
534
+ typeof skillData === 'object' && skillData.basePath ? skillData.basePath : ''
535
+
536
+ combinedSkillsContent += `\n\n--- SKILL EXTERNAL: ${skillName.toUpperCase()} ---\n`
537
+ if (basePath) {
538
+ combinedSkillsContent += `[LOKASI ABSOLUT SKILL INI (Base Path): ${basePath}]\n\n`
539
+ }
540
+ combinedSkillsContent += `${content}`
541
+
542
+ loadedSkills.push(skillName)
543
+ userText = userText.replace(match, '') // Hapus slash command dari teks yang dilihat AI
544
+ }
545
+ } catch (e) {
546
+ console.error('[InputBar] Failed to read skill:', skillName, e)
547
+ }
548
+ }
549
+
550
+ userText = userText.trim()
551
+
552
+ if (loadedSkills.length > 0) {
553
+ finalPrompt = `${userText}\n\n=== SYSTEM INSTRUCTION: SKILL DIAKTIFKAN ===\nBerikut adalah instruksi skill khusus yang WAJIB kamu kombinasikan dan ikuti secara ketat untuk mengeksekusi permintaan di atas. Jika skill memiliki referensi sub-file, kamu BISA membacanya menggunakan tool "read-file" dengan menggabungkan "LOKASI ABSOLUT" di bawah ini beserta path relatifnya:\n${combinedSkillsContent}\n=========================================`
554
+ }
555
+ }
556
+
557
+ const currentAttachments = [...attachedFiles]
558
+ if (attachedFiles.length > 0) {
559
+ const filePathsText = attachedFiles.map((f) => `"${f.path}"`).join(', ')
560
+ if (finalPrompt.trim()) {
561
+ finalPrompt = `${finalPrompt.trim()}\n\n[FILE TERLAMPIR]: ${filePathsText}`
562
+ } else {
563
+ finalPrompt = `Tolong proses/rangkum file terlampir ini.\n\n[FILE TERLAMPIR]: ${filePathsText}`
564
+ }
565
+ setAttachedFiles([])
566
+ }
567
+
568
+ if (finalPrompt.trim()) {
569
+ if (!isLoading) {
570
+ lastPromptRef.current = inputText
571
+ }
572
+ const rawUserText = inputText.trim()
573
+ setInputText('')
574
+ if (typeof onSubmit === 'function') {
575
+ onSubmit(finalPrompt, {
576
+ displayPrompt: rawUserText,
577
+ attachedFiles: currentAttachments
578
+ })
579
+ }
580
+ }
581
+ }
582
+
583
+ const handleEmojiClick = (emoji) => {
584
+ setInputText((prev) => prev + emoji)
585
+ setShowEmojiPicker(false)
586
+ setTimeout(() => {
587
+ if (inputRef.current) inputRef.current.focus()
588
+ }, 50)
589
+ }
590
+
591
+ const fetchWorkspaceFiles = async (query = '') => {
592
+ if (window.api && window.api.getWorkspaceFiles) {
593
+ setIsLoadingFiles(true)
594
+ try {
595
+ const files = await window.api.getWorkspaceFiles(query, workspaceRoot)
596
+ setWorkspaceFiles(files || [])
597
+ } catch (err) {
598
+ console.error('[InputBar] getWorkspaceFiles error:', err)
599
+ } finally {
600
+ setIsLoadingFiles(false)
601
+ }
602
+ }
603
+ }
604
+
605
+ const mentionDisplayItems = useMemo(() => {
606
+ if (mentionCategory === 'all' && !mentionQuery.trim()) {
607
+ return [
608
+ {
609
+ type: 'category',
610
+ category: 'files',
611
+ title: 'Files',
612
+ subtitle: 'Cari berkas di workspace'
613
+ },
614
+ {
615
+ type: 'category',
616
+ category: 'tools',
617
+ title: 'Tool Groups',
618
+ subtitle: 'Kelompok kapabilitas tool agen'
619
+ }
620
+ ]
621
+ }
622
+
623
+ const q = mentionQuery.toLowerCase().trim()
624
+ const items = []
625
+
626
+ // 1. Tool Groups
627
+ if (mentionCategory === 'all' || mentionCategory === 'tools') {
628
+ const matchedTools = toolGroups
629
+ .filter(
630
+ (t) =>
631
+ !q ||
632
+ t.name.toLowerCase().includes(q) ||
633
+ t.label.toLowerCase().includes(q) ||
634
+ t.description.toLowerCase().includes(q)
635
+ )
636
+ .map((t) => ({
637
+ type: 'tool',
638
+ name: t.name,
639
+ label: t.label,
640
+ description: t.description
641
+ }))
642
+ items.push(...matchedTools)
643
+ }
644
+
645
+ // 2. Files
646
+ if (mentionCategory === 'all' || mentionCategory === 'files') {
647
+ const matchedFiles = workspaceFiles
648
+ .filter(
649
+ (f) => !q || f.name.toLowerCase().includes(q) || f.relativePath.toLowerCase().includes(q)
650
+ )
651
+ .map((f) => ({
652
+ type: 'file',
653
+ name: f.name,
654
+ relativePath: f.relativePath,
655
+ absolutePath: f.absolutePath,
656
+ size: f.size,
657
+ ext: f.ext
658
+ }))
659
+ items.push(...matchedFiles)
660
+ }
661
+
662
+ return items
663
+ }, [mentionCategory, mentionQuery, toolGroups, workspaceFiles])
664
+
665
+ const selectMentionItem = (item) => {
666
+ if (!item) return
667
+
668
+ if (item.type === 'category') {
669
+ setMentionCategory(item.category)
670
+ setSelectedMentionIndex(0)
671
+ if (item.category === 'files') {
672
+ fetchWorkspaceFiles('')
673
+ } else if (item.category === 'tools' && toolGroups.length === 0) {
674
+ loadToolGroups()
675
+ }
676
+ if (inputRef.current) inputRef.current.focus()
677
+ return
678
+ }
679
+
680
+ const val = inputText
681
+ const beforeAt = val.slice(0, mentionCursorIndex)
682
+ const afterMention = val.slice(mentionCursorIndex + 1 + mentionQuery.length)
683
+
684
+ if (item.type === 'file') {
685
+ const insertTag = `@${item.relativePath} `
686
+ const nextText = `${beforeAt}${insertTag}${afterMention}`
687
+ setInputText(nextText)
688
+
689
+ // Lampirkan otomatis ke attachedFiles
690
+ const newAttached = {
691
+ name: item.name,
692
+ path: item.absolutePath,
693
+ size: item.size,
694
+ type: item.ext ? `text/${item.ext.replace('.', '')}` : '',
695
+ previewUrl: null
696
+ }
697
+ setAttachedFiles((prev) => {
698
+ const exists = prev.some((f) => f.path === newAttached.path)
699
+ return exists ? prev : [...prev, newAttached]
700
+ })
701
+
702
+ setShowMentionList(false)
703
+ setMentionCategory('all')
704
+ setMentionQuery('')
705
+ setTimeout(() => {
706
+ if (inputRef.current) {
707
+ const newPos = beforeAt.length + insertTag.length
708
+ inputRef.current.focus()
709
+ inputRef.current.setSelectionRange(newPos, newPos)
710
+ }
711
+ }, 50)
712
+ return
713
+ }
714
+
715
+ if (item.type === 'tool') {
716
+ const insertTag = `@${item.name} `
717
+ const nextText = `${beforeAt}${insertTag}${afterMention}`
718
+ setInputText(nextText)
719
+
720
+ setShowMentionList(false)
721
+ setMentionCategory('all')
722
+ setMentionQuery('')
723
+ setTimeout(() => {
724
+ if (inputRef.current) {
725
+ const newPos = beforeAt.length + insertTag.length
726
+ inputRef.current.focus()
727
+ inputRef.current.setSelectionRange(newPos, newPos)
728
+ }
729
+ }, 50)
730
+ return
731
+ }
732
+ }
733
+
734
+ const handleTextChange = async (e) => {
735
+ const val = e.target.value
736
+ setInputText(val)
737
+
738
+ const cursor = e.target.selectionStart ?? val.length
739
+ const textBefore = val.slice(0, cursor)
740
+
741
+ // 1. Deteksi Slash Command (/):
742
+ if (val.startsWith('/')) {
743
+ const currentSkills = skills && skills.length > 0 ? skills : await reloadSkills()
744
+ const query = val.slice(1).toLowerCase()
745
+ const matches = currentSkills.filter((s) => s.name.toLowerCase().includes(query))
746
+ setFilteredSkills(matches)
747
+ setShowSkillList(true)
748
+ setSelectedSkillIndex(0)
749
+ setShowMentionList(false)
750
+ return
751
+ } else {
752
+ setShowSkillList(false)
753
+ }
754
+
755
+ // 2. Deteksi Mention Tag (@):
756
+ const atIndex = textBefore.lastIndexOf('@')
757
+ if (atIndex !== -1 && (atIndex === 0 || /\s/.test(textBefore[atIndex - 1]))) {
758
+ const afterAt = textBefore.slice(atIndex + 1)
759
+ if (!/\s/.test(afterAt)) {
760
+ if (toolGroups.length === 0) {
761
+ loadToolGroups()
762
+ }
763
+ setShowMentionList(true)
764
+ setMentionQuery(afterAt)
765
+ setMentionCursorIndex(atIndex)
766
+ setSelectedMentionIndex(0)
767
+ fetchWorkspaceFiles(afterAt)
768
+ return
769
+ }
770
+ }
771
+
772
+ setShowMentionList(false)
773
+ setMentionCategory('all')
774
+ }
775
+
776
+ const selectSkill = (skillObj) => {
777
+ setInputText(`/${skillObj.name} `)
778
+ setShowSkillList(false)
779
+ if (inputRef.current) inputRef.current.focus()
780
+ }
781
+
782
+ const handleKeyDown = (e) => {
783
+ // 1. Skill List Navigation
784
+ if (showSkillList && filteredSkills.length > 0) {
785
+ if (e.key === 'ArrowDown') {
786
+ e.preventDefault()
787
+ setSelectedSkillIndex((prev) => (prev + 1) % filteredSkills.length)
788
+ return
789
+ }
790
+ if (e.key === 'ArrowUp') {
791
+ e.preventDefault()
792
+ setSelectedSkillIndex((prev) => (prev - 1 + filteredSkills.length) % filteredSkills.length)
793
+ return
794
+ }
795
+ if (e.key === 'Enter' || e.key === 'Tab') {
796
+ e.preventDefault()
797
+ selectSkill(filteredSkills[selectedSkillIndex])
798
+ return
799
+ }
800
+ if (e.key === 'Escape') {
801
+ setShowSkillList(false)
802
+ return
803
+ }
804
+ }
805
+
806
+ // 2. Mention (@) Navigation
807
+ if (showMentionList && mentionDisplayItems.length > 0) {
808
+ if (e.key === 'ArrowDown') {
809
+ e.preventDefault()
810
+ setSelectedMentionIndex((prev) => (prev + 1) % mentionDisplayItems.length)
811
+ return
812
+ }
813
+ if (e.key === 'ArrowUp') {
814
+ e.preventDefault()
815
+ setSelectedMentionIndex(
816
+ (prev) => (prev - 1 + mentionDisplayItems.length) % mentionDisplayItems.length
817
+ )
818
+ return
819
+ }
820
+ if (e.key === 'Enter' || e.key === 'Tab') {
821
+ e.preventDefault()
822
+ const selected = mentionDisplayItems[selectedMentionIndex]
823
+ if (selected) {
824
+ selectMentionItem(selected)
825
+ }
826
+ return
827
+ }
828
+ if (e.key === 'Escape') {
829
+ e.preventDefault()
830
+ setShowMentionList(false)
831
+ setMentionCategory('all')
832
+ return
833
+ }
834
+ if (e.key === 'Backspace' && mentionCategory !== 'all' && mentionQuery === '') {
835
+ e.preventDefault()
836
+ setMentionCategory('all')
837
+ setSelectedMentionIndex(0)
838
+ return
839
+ }
840
+ }
841
+
842
+ // 3. Normal Send on Enter
843
+ if (e.key === 'Enter' && !e.shiftKey) {
844
+ e.preventDefault()
845
+ if (!isSendDisabled) {
846
+ handleFormSubmit()
847
+ }
848
+ }
849
+ }
850
+
851
+ const isSendDisabled = !inputText.trim() && attachedFiles.length === 0
852
+
853
+ return (
854
+ <div
855
+ className={
856
+ className
857
+ ? className
858
+ : inline
859
+ ? 'w-full max-w-4xl mx-auto relative z-10'
860
+ : 'fixed bottom-8 left-1/2 -translate-x-1/2 w-full max-w-2xl px-4 z-50'
861
+ }
862
+ >
863
+ {/* File Attachment Pills Preview */}
864
+ {attachedFiles.length > 0 && (
865
+ <div className="mb-2 flex items-center gap-2 overflow-x-auto py-1 px-2 no-scrollbar animate-[holo-project-in_0.2s_ease-out_forwards]">
866
+ {attachedFiles.map((file, idx) => (
867
+ <div
868
+ key={file.path + idx}
869
+ className="flex items-center gap-2 bg-[var(--glass-bg)] backdrop-blur-xl border border-[var(--glass-border)] rounded-full px-3 py-1.5 text-xs text-white shadow-lg animate-fade-in group hover:border-primary/50 transition-all flex-shrink-0"
870
+ >
871
+ {file.previewUrl ? (
872
+ <img
873
+ src={file.previewUrl}
874
+ alt={file.name}
875
+ className="w-4.5 h-4.5 rounded-full object-cover border border-white/20 shrink-0"
876
+ />
877
+ ) : (
878
+ <span className="text-sm shrink-0">{getFileIcon(file.name)}</span>
879
+ )}
880
+ <span className="max-w-[140px] truncate font-medium">{file.name}</span>
881
+ {file.size > 0 && (
882
+ <span className="text-[10px] text-white/40">{formatFileSize(file.size)}</span>
883
+ )}
884
+ <button
885
+ type="button"
886
+ onClick={() => removeFile(idx)}
887
+ className="text-white/40 hover:text-error hover:bg-error/20 p-1 rounded-full transition-all cursor-pointer"
888
+ title="Hapus Lampiran"
889
+ >
890
+ <FaTimes size={10} />
891
+ </button>
892
+ </div>
893
+ ))}
894
+ </div>
895
+ )}
896
+
897
+ {/* Workspace Root Active Indicator Pill */}
898
+ {workspaceRoot && (
899
+ <div className="mb-2 flex items-center gap-2 px-3 py-1 bg-base-200/80 border border-primary/30 rounded-lg text-xs text-white/80 w-fit backdrop-blur-md animate-fade-in shadow-md">
900
+ <FaFolder className="text-primary text-xs" />
901
+ <span className="text-[10px] text-primary uppercase font-bold tracking-wider">
902
+ Workspace:
903
+ </span>
904
+ <span className="font-mono text-[11px] truncate max-w-xs">{workspaceRoot}</span>
905
+ {onSelectWorkspace && (
906
+ <button
907
+ type="button"
908
+ onClick={onSelectWorkspace}
909
+ className="ml-1 text-[10px] text-white/50 hover:text-primary transition-colors cursor-pointer underline"
910
+ title="Ganti folder proyek"
911
+ >
912
+ Ganti
913
+ </button>
914
+ )}
915
+ </div>
916
+ )}
917
+
918
+ {/* Hidden Native File Input */}
919
+ <input
920
+ ref={fileInputRef}
921
+ type="file"
922
+ multiple
923
+ className="hidden"
924
+ onChange={handleFileChange}
925
+ />
926
+
927
+ <form
928
+ onSubmit={(e) => {
929
+ e.preventDefault()
930
+ handleFormSubmit()
931
+ }}
932
+ onDragOver={handleDragOver}
933
+ onDragLeave={handleDragLeave}
934
+ onDrop={handleDrop}
935
+ className={`relative flex items-center bg-[var(--glass-bg)] border rounded-lg p-2 pr-3 shadow-[0_8px_32px_rgba(0,0,0,0.3)] transition-colors duration-300 focus-within:border-primary/50 focus-within:shadow-[0_0_20px_oklch(var(--p)/0.2)] ${
936
+ isDragging
937
+ ? 'border-primary bg-primary/10 shadow-[0_0_30px_oklch(var(--p)/0.3)] scale-[1.02]'
938
+ : 'border-[var(--glass-border)]'
939
+ }`}
940
+ >
941
+ {/* Drag & Drop Overlay Indicator */}
942
+ {isDragging && (
943
+ <div className="absolute inset-0 rounded-lg bg-primary/20 backdrop-blur-md border-2 border-dashed border-primary flex items-center justify-center z-50 pointer-events-none text-white font-medium gap-2 animate-pulse">
944
+ <FaPaperclip className="animate-bounce" size={20} />
945
+ <span>Lepaskan file di sini untuk melampirkan...</span>
946
+ </div>
947
+ )}
948
+
949
+ {/* Workspace Root Folder Button */}
950
+ {onSelectWorkspace && (
951
+ <button
952
+ type="button"
953
+ onClick={onSelectWorkspace}
954
+ className={`p-3 rounded-full transition-all flex-shrink-0 relative ${
955
+ workspaceRoot
956
+ ? 'text-primary hover:text-primary hover:bg-primary/10'
957
+ : 'text-white/40 hover:text-white/80 hover:bg-white/5'
958
+ }`}
959
+ title={
960
+ workspaceRoot
961
+ ? `Workspace Root: ${workspaceRoot} (Klik untuk ganti)`
962
+ : 'Atur Folder Proyek (Workspace Root)'
963
+ }
964
+ >
965
+ <FaFolder size={16} />
966
+ {workspaceRoot && (
967
+ <span className="absolute bottom-2 right-2 w-1.5 h-1.5 rounded-full bg-primary ring-1 ring-base-300" />
968
+ )}
969
+ </button>
970
+ )}
971
+
972
+ {/* Paperclip File Upload Button */}
973
+ <button
974
+ type="button"
975
+ onClick={handlePaperclipClick}
976
+ className="p-3 text-white/40 hover:text-white/80 hover:bg-white/5 rounded-full transition-all flex-shrink-0"
977
+ title="Lampirkan File (PDF, DOCX, TXT, MD, Gambar, dll)"
978
+ >
979
+ <FaPaperclip size={16} />
980
+ </button>
981
+
982
+ {/* Mic / Record Toggle (Hold to talk) */}
983
+ <button
984
+ type="button"
985
+ onClick={isRecording ? onStopRecord : onStartRecord}
986
+ disabled={isProcessing || isLoading}
987
+ className={`relative p-3 md:p-4 rounded-full flex-shrink-0 transition-all duration-300 transform outline-none z-10 ${
988
+ isProcessing
989
+ ? 'text-primary bg-primary/20 cursor-wait'
990
+ : isLoading
991
+ ? 'text-white/20 bg-white/5 cursor-not-allowed'
992
+ : isRecording
993
+ ? 'text-error bg-error/20'
994
+ : 'text-white/40 hover:text-white/80 hover:bg-white/5'
995
+ }`}
996
+ style={{
997
+ transform: isRecording && !isProcessing ? `scale(${1 + audioIntensity * 0.3})` : '',
998
+ boxShadow:
999
+ isRecording && !isProcessing
1000
+ ? `0 0 ${10 + audioIntensity * 40}px rgba(255,0,0, ${0.3 + audioIntensity * 0.5})`
1001
+ : ''
1002
+ }}
1003
+ title={
1004
+ isProcessing
1005
+ ? 'Sedang memproses suara...'
1006
+ : isLoading
1007
+ ? 'Agen sedang sibuk'
1008
+ : 'Mulai/Berhenti Rekam (Ctrl+Alt+M)'
1009
+ }
1010
+ >
1011
+ {isRecording && !isProcessing && (
1012
+ <div
1013
+ className="absolute inset-0 rounded-full bg-error/30 -z-10"
1014
+ style={{ transform: `scale(${1 + audioIntensity * 0.8})` }}
1015
+ />
1016
+ )}
1017
+ {isProcessing ? (
1018
+ <span className="loading loading-spinner w-[18px] h-[18px]"></span>
1019
+ ) : isLoading ? (
1020
+ <FaLock size={18} />
1021
+ ) : (
1022
+ <FaMicrophone size={18} />
1023
+ )}
1024
+ </button>
1025
+
1026
+ {/* Emoji Button */}
1027
+ <div className="relative flex-shrink-0">
1028
+ <button
1029
+ type="button"
1030
+ onClick={() => setShowEmojiPicker(!showEmojiPicker)}
1031
+ className="p-3 text-white/40 hover:text-white/80 hover:bg-white/5 rounded-full transition-all"
1032
+ title="Insert Emoji"
1033
+ >
1034
+ <FaSmile size={18} />
1035
+ </button>
1036
+
1037
+ {showEmojiPicker && (
1038
+ <div className="absolute bottom-full left-0 mb-4 bg-[var(--glass-bg)] backdrop-blur-3xl border border-[var(--glass-border)] rounded-2xl p-2 shadow-2xl flex flex-wrap w-52 gap-1 z-[100] animate-[holo-project-in_0.2s_ease-out_forwards]">
1039
+ {EMOJIS.map((emoji) => (
1040
+ <button
1041
+ key={emoji}
1042
+ type="button"
1043
+ onClick={() => handleEmojiClick(emoji)}
1044
+ className="w-10 h-10 flex items-center justify-center hover:bg-white/10 rounded-xl text-2xl transition-all hover:scale-110 active:scale-95"
1045
+ >
1046
+ {emoji}
1047
+ </button>
1048
+ ))}
1049
+ </div>
1050
+ )}
1051
+ </div>
1052
+ {/* Skill Autocomplete Dropdown */}
1053
+ {showSkillList && filteredSkills.length > 0 && (
1054
+ <div className="absolute bottom-full left-12 mb-2 w-[400px] bg-base-300/95 backdrop-blur-xl border border-[var(--glass-border)] rounded-xl overflow-hidden shadow-[0_8px_32px_rgba(0,0,0,0.5)] z-50 animate-fade-in">
1055
+ <div className="p-2 text-xs font-bold text-gray-500 uppercase tracking-wider border-b border-white/5">
1056
+ Available Skills
1057
+ </div>
1058
+ <div ref={skillListRef} className="max-h-64 overflow-y-auto no-scrollbar">
1059
+ {filteredSkills.map((skillObj, idx) => (
1060
+ <div
1061
+ key={skillObj.name}
1062
+ onClick={() => selectSkill(skillObj)}
1063
+ className={`px-4 py-3 cursor-pointer transition-colors flex flex-col gap-1 border-b border-white/5 last:border-0 ${
1064
+ idx === selectedSkillIndex
1065
+ ? 'bg-primary-500/20 text-primary-400'
1066
+ : 'hover:bg-white/10 text-gray-300'
1067
+ }`}
1068
+ >
1069
+ <div className="font-semibold text-sm">/{skillObj.name}</div>
1070
+ <div
1071
+ className={`text-xs ${idx === selectedSkillIndex ? 'text-primary-400/80' : 'text-gray-400'} line-clamp-2`}
1072
+ >
1073
+ {skillObj.description}
1074
+ </div>
1075
+ </div>
1076
+ ))}
1077
+ </div>
1078
+ </div>
1079
+ )}
1080
+
1081
+ {/* Mention (@) Autocomplete Dropdown */}
1082
+ {showMentionList && (mentionDisplayItems.length > 0 || isLoadingFiles) && (
1083
+ <div className="absolute bottom-full left-4 mb-2 w-[380px] max-w-[calc(100vw-2rem)] bg-base-300/95 backdrop-blur-xl border border-[var(--glass-border)] rounded-xl p-1.5 shadow-[0_8px_32px_rgba(0,0,0,0.5)] z-50 animate-fade-in flex flex-col">
1084
+ {/* Items List */}
1085
+ <div ref={mentionListRef} className="max-h-64 overflow-y-auto no-scrollbar">
1086
+ {mentionDisplayItems.length === 0 && !isLoadingFiles ? (
1087
+ <div className="py-3 text-center text-xs text-gray-400">Tidak ada yang cocok.</div>
1088
+ ) : (
1089
+ mentionDisplayItems.map((item, idx) => (
1090
+ <div
1091
+ key={
1092
+ item.type === 'category'
1093
+ ? item.category
1094
+ : item.type === 'file'
1095
+ ? item.absolutePath
1096
+ : item.name
1097
+ }
1098
+ onClick={() => selectMentionItem(item)}
1099
+ className={`px-3 py-1.5 cursor-pointer transition-colors rounded-lg flex items-center gap-2.5 ${
1100
+ idx === selectedMentionIndex
1101
+ ? 'bg-primary/20 text-primary'
1102
+ : 'hover:bg-white/5 text-gray-300'
1103
+ }`}
1104
+ >
1105
+ {/* Inline Icon */}
1106
+ <div className="shrink-0 text-sm opacity-80">
1107
+ {item.type === 'category' ? (
1108
+ item.category === 'files' ? (
1109
+ <Folder size={15} />
1110
+ ) : (
1111
+ <Wrench size={15} />
1112
+ )
1113
+ ) : item.type === 'file' ? (
1114
+ getFileIcon(item.name)
1115
+ ) : (
1116
+ <Wrench size={15} />
1117
+ )}
1118
+ </div>
1119
+
1120
+ {/* Text Info */}
1121
+ <div className="flex-1 min-w-0">
1122
+ <div className="font-medium text-xs truncate">
1123
+ {item.type === 'category'
1124
+ ? item.title
1125
+ : item.type === 'file'
1126
+ ? item.relativePath
1127
+ : `@${item.name}`}
1128
+ </div>
1129
+ <div className="text-[11px] text-gray-400 truncate">
1130
+ {item.type === 'category'
1131
+ ? item.subtitle
1132
+ : item.type === 'file'
1133
+ ? formatFileSize(item.size)
1134
+ : item.description || item.label}
1135
+ </div>
1136
+ </div>
1137
+ </div>
1138
+ ))
1139
+ )}
1140
+ </div>
1141
+ </div>
1142
+ )}
1143
+
1144
+ {/* Input Textarea */}
1145
+ <textarea
1146
+ ref={inputRef}
1147
+ rows={1}
1148
+ value={inputText}
1149
+ onChange={handleTextChange}
1150
+ onKeyDown={handleKeyDown}
1151
+ onPaste={handlePaste}
1152
+ placeholder={
1153
+ isLoading
1154
+ ? 'Beri intervensi ke Mark...'
1155
+ : attachedFiles.length > 0
1156
+ ? 'Tambah instruksi untuk file terlampir...'
1157
+ : 'Tanya apapun ke Mark...'
1158
+ }
1159
+ className="flex-1 resize-none bg-transparent border-none outline-none text-white px-3 py-2.5 text-sm md:text-base leading-normal placeholder:text-white/30 disabled:opacity-50 no-scrollbar"
1160
+ />
1161
+
1162
+ {/* Action Buttons */}
1163
+ <div className="flex items-center gap-2 flex-shrink-0">
1164
+ {/* Auto Mode Toggle Button */}
1165
+ <button
1166
+ type="button"
1167
+ onClick={handleToggleAutoMode}
1168
+ className={`relative flex items-center justify-center gap-1 px-2.5 py-1 rounded-full text-xs font-mono transition-all duration-200 cursor-pointer select-none outline-none btn btn-circle ${
1169
+ isAutoMode
1170
+ ? 'bg-warning/20 text-warning border border-warning/40 font-semibold'
1171
+ : 'text-white/30 hover:text-white/70 hover:bg-white/5 border border-transparent font-normal'
1172
+ }`}
1173
+ title={
1174
+ isAutoMode
1175
+ ? 'Auto Mode: AKTIF. Semua persetujuan tool otomatis diizinkan untuk sesi ini.'
1176
+ : 'Auto Mode: NONAKTIF. Klik untuk otomatis menyetujui semua tool di sesi ini.'
1177
+ }
1178
+ >
1179
+ <Zap className={`w-3 h-3 ${isAutoMode ? 'fill-white text-white' : ''}`} />
1180
+ </button>
1181
+
1182
+ {/* Ring Gauge Context Indicator (~30px) */}
1183
+ {(() => {
1184
+ const pct = Math.min(100, Math.max(0, contextTracker.percentage || 0))
1185
+ const roundedPct =
1186
+ (contextTracker.currentTokens || contextTracker.currentChars || 0) > 0
1187
+ ? Math.max(1, Math.round(pct))
1188
+ : Math.round(pct)
1189
+ const radius = 14
1190
+ const circumference = 2 * Math.PI * radius
1191
+ const strokeDashoffset = circumference - (pct / 100) * circumference
1192
+ const colorClass =
1193
+ pct >= 85 ? 'stroke-rose-500' : pct >= 50 ? 'stroke-amber-400' : 'stroke-emerald-400'
1194
+ const barColorClass =
1195
+ pct >= 85 ? 'bg-rose-500' : pct >= 50 ? 'bg-amber-400' : 'bg-emerald-400'
1196
+
1197
+ return (
1198
+ <div className="relative flex items-center justify-center px-1 select-none">
1199
+ {/* Ring Gauge Trigger Button */}
1200
+ <div
1201
+ ref={gaugeRef}
1202
+ role="button"
1203
+ tabIndex={0}
1204
+ onClick={() => setShowContextPopover((prev) => !prev)}
1205
+ onKeyDown={(e) => {
1206
+ if (e.key === 'Enter' || e.key === ' ') setShowContextPopover((prev) => !prev)
1207
+ }}
1208
+ className="relative w-[30px] h-[30px] flex items-center justify-center cursor-pointer transition-transform duration-200 hover:scale-110 group/gauge outline-none"
1209
+ title="Context Window Info (Dual-Layer Hermes Compressor)"
1210
+ >
1211
+ <svg className="w-full h-full -rotate-90" viewBox="0 0 36 36">
1212
+ <circle
1213
+ cx="18"
1214
+ cy="18"
1215
+ r={radius}
1216
+ className="stroke-white/10"
1217
+ strokeWidth="2.5"
1218
+ fill="none"
1219
+ />
1220
+ {pct > 0 && (
1221
+ <circle
1222
+ cx="18"
1223
+ cy="18"
1224
+ r={radius}
1225
+ className={`${colorClass} transition-all duration-500 ease-out`}
1226
+ strokeWidth="2.5"
1227
+ strokeDasharray={circumference}
1228
+ strokeDashoffset={strokeDashoffset}
1229
+ strokeLinecap="round"
1230
+ fill="none"
1231
+ />
1232
+ )}
1233
+ </svg>
1234
+ <span className="absolute inset-0 flex items-center justify-center font-mono text-[8px] font-bold text-white/80 group-hover/gauge:text-white tracking-tight leading-none pointer-events-none">
1235
+ {roundedPct}%
1236
+ </span>
1237
+ </div>
1238
+
1239
+ {/* Popover Card Overlay */}
1240
+ {showContextPopover && (
1241
+ <div
1242
+ ref={contextPopoverRef}
1243
+ className="absolute bottom-full right-0 mb-3.5 w-64 p-3.5 bg-base-200 border border-primary/30 rounded-xl shadow-2xl z-50 animate-fade-in text-left cursor-default"
1244
+ onClick={(e) => e.stopPropagation()}
1245
+ >
1246
+ <div className="flex items-center justify-between text-[11px] font-medium text-white/40 mb-0.5">
1247
+ <span>Session Info</span>
1248
+ <span className="text-[9px] uppercase tracking-wider text-primary font-bold">
1249
+ In-Place
1250
+ </span>
1251
+ </div>
1252
+ <div className="text-xs font-bold text-white mb-2.5">Context Window</div>
1253
+
1254
+ <div className="flex items-center justify-between text-xs font-semibold mb-1.5 font-mono">
1255
+ <span className="text-white">
1256
+ {(contextTracker.currentTokens || contextTracker.currentChars || 0) >= 1000
1257
+ ? `${((contextTracker.currentTokens || contextTracker.currentChars || 0) / 1000).toFixed(1)}K`
1258
+ : contextTracker.currentTokens || contextTracker.currentChars || 0}{' '}
1259
+ / 256K tokens
1260
+ </span>
1261
+ <span className="text-white/60">{roundedPct}%</span>
1262
+ </div>
1263
+
1264
+ {/* Progress Bar */}
1265
+ <div className="w-full h-1.5 bg-white/10 rounded-full overflow-hidden mb-1.5">
1266
+ <div
1267
+ className={`h-full ${barColorClass} transition-all duration-300 rounded-full`}
1268
+ style={{ width: `${Math.min(100, Math.max(pct, 2))}%` }}
1269
+ />
1270
+ </div>
1271
+
1272
+ <div className="text-[10px] text-white/40 mb-2 font-mono">
1273
+ {pct >= 85
1274
+ ? 'Gateway Hygiene Active (>=85%)'
1275
+ : pct >= 50
1276
+ ? 'In-Loop Compressor Guard (>=50%)'
1277
+ : 'Optimal Context Load'}
1278
+ </div>
1279
+
1280
+ {/* Compact Conversation Button */}
1281
+ <button
1282
+ type="button"
1283
+ onClick={() => {
1284
+ setShowContextPopover(false)
1285
+ if (typeof onManualCompact === 'function') {
1286
+ onManualCompact()
1287
+ } else {
1288
+ window.dispatchEvent(new CustomEvent('request-manual-compaction'))
1289
+ }
1290
+ }}
1291
+ disabled={isLoading}
1292
+ className="w-full py-2 px-3 rounded-lg bg-white/5 hover:bg-white/10 active:bg-white/15 border border-primary/30 text-xs font-medium text-white transition-all flex items-center justify-center gap-1.5 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
1293
+ >
1294
+ <span>Compact Conversation</span>
1295
+ </button>
1296
+
1297
+ {/* Caret pointing to gauge */}
1298
+ <div className="absolute -bottom-1.5 right-3.5 w-3 h-3 bg-base-200 border-r border-b border-primary/30 rotate-45" />
1299
+ </div>
1300
+ )}
1301
+ </div>
1302
+ )
1303
+ })()}
1304
+
1305
+ {isLoading && (
1306
+ <button
1307
+ type="button"
1308
+ onClick={() => setShowAbortConfirm(true)}
1309
+ className="p-3 rounded-full bg-error/20 text-error hover:bg-error hover:text-white transition-all"
1310
+ title="Stop Generation (Hard Abort)"
1311
+ >
1312
+ <FaStop size={16} />
1313
+ </button>
1314
+ )}
1315
+ <button
1316
+ type="submit"
1317
+ disabled={isSendDisabled}
1318
+ className="p-3 rounded-full bg-success text-success-content disabled:opacity-30 disabled:bg-white/10 disabled:text-white/30 hover:bg-success/80 hover:scale-105 active:scale-95 transition-all"
1319
+ title="Send Message"
1320
+ >
1321
+ <FaArrowUp size={16} />
1322
+ </button>
1323
+ </div>
1324
+ </form>
1325
+
1326
+ <ConfirmModal
1327
+ isOpen={showAbortConfirm}
1328
+ title="Hard Abort Proses?"
1329
+ message="Yakin mau memberhentikan proses Mark secara paksa? Tindakan ini akan menghentikan secara langsung semua alat yang sedang berjalan dan memutuskan koneksi ke otak AI-nya seketika."
1330
+ confirmText="Berhentikan"
1331
+ cancelText="Batal"
1332
+ isError={true}
1333
+ onConfirm={() => {
1334
+ setShowAbortConfirm(false)
1335
+ if (lastPromptRef.current) {
1336
+ setInputText(lastPromptRef.current)
1337
+ }
1338
+ if (onStop) onStop()
1339
+ }}
1340
+ onCancel={() => setShowAbortConfirm(false)}
1341
+ />
1342
+ </div>
1343
+ )
1344
+ }
1345
+
1346
+ export default memo(InputBar)