@adiholick/nvstudio-mcp 2.0.3 → 2.1.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.
@@ -1,150 +1,133 @@
1
- local HttpService = game:GetService("HttpService")
2
- local LogService = game:GetService("LogService")
1
+ -- ══════════════════════════════════════════════════════════════════════
2
+ -- NVStudio MCP Plugin v2.1.0
3
+ -- Arsitektur: SSE (Server-Sent Events) via WebStreamClient
4
+ -- Plugin membuka koneksi stream persisten ke server Node.js.
5
+ -- Server PUSH task langsung — tidak ada polling loop.
6
+ -- ══════════════════════════════════════════════════════════════════════
3
7
 
4
- local TASK_URL = "http://localhost:3055/api/tasks"
5
- local RESPONSE_URL = "http://localhost:3055/api/response"
8
+ local HttpService = game:GetService("HttpService")
9
+ local LogService = game:GetService("LogService")
6
10
 
7
- -- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
8
- local STUDIO_ID = HttpService:GenerateGUID(false)
11
+ -- URL Endpoints
12
+ local BASE_URL = "http://localhost:3055"
13
+ local STREAM_URL = BASE_URL .. "/api/stream"
14
+ local RESPONSE_URL = BASE_URL .. "/api/response" -- /api/response/:taskId via POST
15
+ local PING_URL = BASE_URL .. "/api/ping"
9
16
 
10
- -- Import UI Manager (Procedural Dashboard)
17
+ -- ID unik per sesi Studio (jika dua Studio terbuka, keduanya didaftarkan terpisah)
18
+ local STUDIO_ID = HttpService:GenerateGUID(false)
19
+
20
+ -- ── UI Module ──────────────────────────────────────────────────────────────────
11
21
  local uiModule = script.Parent:FindFirstChild("ui_manager")
12
22
  local ui = nil
13
23
 
14
- -- Membangun Tombol Toolbar (Toggle)
15
- local toolbar = plugin:CreateToolbar("NVStudio MCP")
24
+ -- ── Toolbar ───────────────────────────────────────────────────────────────────
25
+ local toolbar = plugin:CreateToolbar("NVStudio MCP")
16
26
  local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722", "NVStudio")
17
27
 
18
- -- Global State & Context AI
28
+ -- ── State ─────────────────────────────────────────────────────────────────────
19
29
  local ctx = {
20
30
  scriptHistory = {},
21
- recentLogs = {},
22
- HttpService = HttpService,
23
- studioId = STUDIO_ID
31
+ recentLogs = {},
32
+ HttpService = HttpService,
33
+ studioId = STUDIO_ID,
24
34
  }
25
- local MAX_LOGS = 50
26
- local isConnected = false
27
- local autoConnect = plugin:GetSetting("AutoConnect") or false
28
- local isPolling = false
29
- local isClickDebounce = false -- Mencegah rapid double-click
35
+ local MAX_LOGS = 50
36
+ local isConnected = false -- apakah SSE stream aktif
37
+ local mcpAgentActive = false -- apakah Antigravity/Cursor sedang konek ke MCP
38
+ local isManualDisconnect = false
39
+ local isClickDebounce = false
40
+ local currentStream = nil -- WebStreamClient aktif
41
+ local streamConnections = {} -- RBXScriptConnection SSE
42
+
43
+ -- Reconnect config (exponential backoff)
44
+ local RECONNECT_DELAY_INITIAL = 0.5
45
+ local RECONNECT_DELAY_MAX = 10
46
+ local reconnectDelay = RECONNECT_DELAY_INITIAL
30
47
 
31
- -- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
32
- local startPolling
33
- local stopPolling
48
+ -- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
49
+ local skills = {}
50
+ for _, module in ipairs(script.Parent:GetChildren()) do
51
+ if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
52
+ local ok, result = pcall(require, module)
53
+ if ok then
54
+ skills[module.Name] = result
55
+ else
56
+ warn("[NVStudio] Gagal load module '" .. module.Name .. "': " .. tostring(result))
57
+ end
58
+ end
59
+ end
34
60
 
35
- -- Inisialisasi UI jika modul ditemukan
61
+ -- ── Inisialisasi UI ──────────────────────────────────────────────────────────
36
62
  if uiModule then
37
63
  local UIManager = require(uiModule)
38
64
  ui = UIManager.init(plugin, STUDIO_ID)
39
-
40
- -- Sync Initial State ke UI
41
- ui:setAutoConnectState(autoConnect)
42
- ui:setStatus("disconnected")
43
-
44
- -- Sync state tombol toolbar dengan status widget
65
+
66
+ ui:setAutoConnectState(true)
67
+ ui:setStatus("waiting")
68
+
45
69
  if ui.widget then
46
70
  toggleBtn:SetActive(ui.widget.Enabled)
47
71
  ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
48
72
  toggleBtn:SetActive(ui.widget.Enabled)
49
73
  end)
50
74
  end
51
-
52
- -- Event Toggle GUI Panel
75
+
53
76
  toggleBtn.Click:Connect(function()
54
77
  ui:toggleVisibility()
55
78
  end)
56
-
57
- -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
79
+
80
+ -- Tombol Connect / Disconnect
58
81
  ui.connectBtn.MouseButton1Click:Connect(function()
59
82
  if isClickDebounce then return end
60
83
  isClickDebounce = true
61
-
84
+
62
85
  if not isConnected then
63
- -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
64
86
  isManualDisconnect = false
65
- isConnected = true
87
+ reconnectDelay = RECONNECT_DELAY_INITIAL
66
88
  ui:setStatus("waiting")
67
- ui:addLog("[System] Koneksi manual dimulai. Mencari AI Server (Waiting)...", Color3.fromRGB(255, 220, 100))
68
- startPolling()
89
+ ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
90
+ task.spawn(connectSSE)
69
91
  else
70
- -- DISCONNECT — hentikan polling dan tandai manual disconnect
71
- stopPolling()
92
+ isManualDisconnect = true
93
+ disconnectSSE()
72
94
  ui:setStatus("disconnected")
73
95
  ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
74
96
  end
75
-
76
- task.delay(0.3, function()
77
- isClickDebounce = false
78
- end)
79
- end)
80
-
81
- -- Event Custom Checkbox (Auto-Connect)
82
- ui.autoConnectBtn.MouseButton1Click:Connect(function()
83
- autoConnect = not autoConnect
84
- plugin:SetSetting("AutoConnect", autoConnect)
85
- ui:setAutoConnectState(autoConnect)
86
- ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
97
+
98
+ task.delay(0.3, function() isClickDebounce = false end)
87
99
  end)
88
100
  else
89
- -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
90
101
  toggleBtn.Click:Connect(function()
91
- print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan di dalam Folder. GUI Gagal dimuat!")
102
+ print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan. GUI gagal dimuat!")
92
103
  end)
93
104
  end
94
105
 
95
- -- Handler Output Studio (Untuk AI dan GUI)
106
+ -- ── Log Capture ──────────────────────────────────────────────────────────────
96
107
  local function addSystemLog(msg, msgType)
97
- local color = Color3.fromRGB(200, 200, 200)
108
+ local color = Color3.fromRGB(200, 200, 200)
98
109
  local prefix = "[INFO] "
99
110
  if msgType == Enum.MessageType.MessageError then
100
- prefix = "[ERROR] "
101
- color = Color3.fromRGB(255, 100, 100)
111
+ prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
102
112
  elseif msgType == Enum.MessageType.MessageWarning then
103
- prefix = "[WARN] "
104
- color = Color3.fromRGB(255, 200, 100)
113
+ prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
105
114
  end
106
-
107
115
  local fullMsg = prefix .. msg
108
-
109
- -- 1. Simpan di memori untuk disedot AI
110
116
  table.insert(ctx.recentLogs, fullMsg)
111
- if #ctx.recentLogs > MAX_LOGS then
112
- table.remove(ctx.recentLogs, 1)
113
- end
114
-
115
- -- 2. Cetak ke Terminal GUI Plugin
116
- if ui then
117
- ui:addLog(fullMsg, color)
118
- end
117
+ if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
118
+ if ui then ui:addLog(fullMsg, color) end
119
119
  end
120
-
121
- -- Ikat listener output
122
120
  LogService.MessageOut:Connect(addSystemLog)
123
121
 
124
- -- Memuat semua module skills secara dinamis
125
- local skills = {}
126
- for _, module in ipairs(script.Parent:GetChildren()) do
127
- if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
128
- skills[module.Name] = require(module)
129
- end
130
- end
131
-
132
- -- Fungsi Helper Pathing
122
+ -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
133
123
  local function getInstanceFromPath(pathString)
134
- local parts = string.split(pathString, ".")
124
+ local parts = string.split(pathString, ".")
135
125
  local current = game
136
-
137
126
  if parts[1] == "game" then table.remove(parts, 1) end
138
-
139
- -- Handle common Services directly to ensure reliability
140
127
  pcall(function()
141
128
  local svc = game:GetService(parts[1])
142
- if svc then
143
- current = svc
144
- table.remove(parts, 1)
145
- end
129
+ if svc then current = svc; table.remove(parts, 1) end
146
130
  end)
147
-
148
131
  for _, part in ipairs(parts) do
149
132
  local found = current:FindFirstChild(part)
150
133
  if not found then return nil end
@@ -153,158 +136,257 @@ local function getInstanceFromPath(pathString)
153
136
  return current
154
137
  end
155
138
 
156
- -- Memproses Task Eksekusi dari AI
139
+ -- ── Proses Task dari Server ───────────────────────────────────────────────────
157
140
  local function processTask(taskData)
158
141
  if not taskData or not taskData.id or not taskData.command then return nil end
159
-
160
- if ui then
161
- ui:addLog("[AI Command] Menerima instruksi: " .. taskData.command, Color3.fromRGB(150, 200, 255))
142
+
143
+ if ui then
144
+ ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
162
145
  end
163
-
146
+
164
147
  local targetInstance = getInstanceFromPath(taskData.target)
165
148
  if not targetInstance then
166
- return {
167
- id = taskData.id,
168
- status = "error",
169
- error = "Target path tidak ditemukan: " .. taskData.target
170
- }
149
+ return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
171
150
  end
172
151
 
173
152
  local skillFunc = skills[taskData.command]
174
153
  if not skillFunc then
175
- return {
176
- id = taskData.id,
177
- status = "error",
178
- error = "Command tidak dikenali oleh Studio: " .. taskData.command
179
- }
154
+ return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
180
155
  end
181
156
 
182
- -- Eksekusi skill AI
183
157
  local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
184
- result.id = taskData.id
185
-
186
- -- Cetak hasil eksekusi ke Terminal GUI
158
+ result.id = taskData.id
159
+
187
160
  if ui then
188
161
  local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
189
162
  ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
190
163
  end
191
-
192
- return result
193
- end
194
-
195
- -- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
196
- local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
197
164
 
198
- function stopPolling()
199
- isConnected = false
200
- isManualDisconnect = true
201
- -- isPolling akan menjadi false secara otomatis saat loop keluar
165
+ return result
202
166
  end
203
167
 
204
- -- Background Polling Loop (Dapat dihentikan oleh user)
205
- function startPolling()
206
- if isPolling then return end
207
- isPolling = true
208
-
168
+ -- ── Kirim Respons ke Server ───────────────────────────────────────────────────
169
+ local function sendResponse(taskId, resultData)
209
170
  task.spawn(function()
210
- while isConnected do
211
- local success, response = pcall(function()
212
- -- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
213
- local cacheBuster = "?t=" .. tostring(os.clock())
214
- return HttpService:GetAsync(TASK_URL .. cacheBuster)
171
+ local ok, err = pcall(function()
172
+ local json = HttpService:JSONEncode(resultData)
173
+ -- Coba endpoint baru dulu (/api/response/:taskId), fallback ke lama
174
+ local success = pcall(function()
175
+ HttpService:PostAsync(
176
+ RESPONSE_URL .. "/" .. taskId,
177
+ json,
178
+ Enum.HttpContentType.ApplicationJson
179
+ )
215
180
  end)
216
-
217
- -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
218
- if not isConnected then
219
- break
181
+ if not success then
182
+ HttpService:PostAsync(RESPONSE_URL, json, Enum.HttpContentType.ApplicationJson)
220
183
  end
184
+ end)
185
+ if not ok and ui then
186
+ ui:addLog("[System] Gagal kirim response: " .. tostring(err), Color3.fromRGB(255, 100, 100))
187
+ end
188
+ end)
189
+ end
221
190
 
222
- if success and response then
223
- -- JIKA PING SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
224
- if ui then ui:setStatus("connected") end
191
+ -- ── Proses satu baris data SSE ────────────────────────────────────────────────
192
+ local function handleSSEMessage(rawMessage)
193
+ -- Strip prefix "data: " jika ada (beberapa Studio version memberikannya)
194
+ local payload = rawMessage
195
+ local normalized = rawMessage:gsub("\r\n", "\n"):gsub("\r", "\n")
196
+ if normalized:sub(1, 5) == "data:" then
197
+ payload = normalized:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
198
+ end
199
+ if payload == "" then return end
225
200
 
226
- local decodeSuccess, taskData = pcall(function()
227
- return HttpService:JSONDecode(response)
228
- end)
229
-
230
- if decodeSuccess and taskData and taskData.id then
231
- local resultData = processTask(taskData)
232
- if resultData then
233
- pcall(function()
234
- local jsonResult = HttpService:JSONEncode(resultData)
235
- HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
236
- end)
237
- end
201
+ local ok, decoded = pcall(function() return HttpService:JSONDecode(payload) end)
202
+ if not ok or type(decoded) ~= "table" then return end
203
+
204
+ local kind = decoded.kind
205
+
206
+ -- ── heartbeat ────────────────────────────────────────────────────
207
+ if kind == "heartbeat" then
208
+ -- Server masih hidup, tidak perlu tindakan khusus
209
+ return
210
+ end
211
+
212
+ -- ── status ───────────────────────────────────────────────────────
213
+ if kind == "status" then
214
+ local newMcpActive = decoded.mcpConnected == true
215
+ if newMcpActive ~= mcpAgentActive then
216
+ mcpAgentActive = newMcpActive
217
+ if ui then
218
+ if mcpAgentActive then
219
+ ui:setStatus("connected")
220
+ ui:addLog("[System] IDE Agent (Antigravity/Cursor) terhubung ke MCP.", Color3.fromRGB(100, 255, 100))
221
+ else
222
+ ui:setStatus("waiting")
223
+ ui:addLog("[System] IDE Agent terputus dari MCP.", Color3.fromRGB(255, 200, 100))
238
224
  end
239
-
240
- -- Fast-polling (long-polling yield di server Node.js)
241
- -- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
242
- task.wait(0.05)
243
- else
244
- -- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
245
- if ui then ui:setStatus("waiting") end
246
- task.wait(1)
247
225
  end
248
-
249
- -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
250
- if not isConnected then
251
- break
226
+ end
227
+ return
228
+ end
229
+
230
+ -- ── request (task dari AI agent) ─────────────────────────────────
231
+ if kind == "request" then
232
+ local taskId = decoded.requestId
233
+ local command = decoded.command
234
+ local target = decoded.target
235
+ local data = decoded.data
236
+
237
+ if not taskId or not command then return end
238
+
239
+ -- Proses task di coroutine terpisah agar tidak memblokir stream
240
+ task.spawn(function()
241
+ local resultData = processTask({
242
+ id = taskId,
243
+ command = command,
244
+ target = target or "",
245
+ data = data,
246
+ })
247
+ if resultData then
248
+ sendResponse(taskId, resultData)
252
249
  end
250
+ end)
251
+ return
252
+ end
253
+ end
254
+
255
+ -- ── Tutup SSE stream yang aktif ───────────────────────────────────────────────
256
+ function disconnectSSE()
257
+ isConnected = false
258
+ for _, conn in ipairs(streamConnections) do
259
+ pcall(function() conn:Disconnect() end)
260
+ end
261
+ streamConnections = {}
262
+ if currentStream then
263
+ pcall(function() currentStream:Close() end)
264
+ currentStream = nil
265
+ end
266
+ end
267
+
268
+ -- ── Buka SSE stream baru ──────────────────────────────────────────────────────
269
+ function connectSSE()
270
+ if isConnected then return end
271
+ disconnectSSE() -- Bersihkan sisa koneksi lama
272
+
273
+ local url = STREAM_URL .. "?studioId=" .. HttpService:UrlEncode(STUDIO_ID)
274
+
275
+ local ok, streamOrErr = pcall(function()
276
+ return HttpService:GetAsync(url, false) -- Gunakan non-cached request
277
+ end)
278
+
279
+ -- WebStreamClient hanya tersedia di Studio; GetAsync biasa tidak akan
280
+ -- mempertahankan koneksi. Kita butuh WebStreamClient khusus.
281
+ -- Coba via WebStreamClient API:
282
+ local streamOk, stream = pcall(function()
283
+ -- @ts-ignore – WebStreamClient mungkin belum ada di semua versi Studio
284
+ return (HttpService :: any):WebStream(url)
285
+ end)
286
+
287
+ if not streamOk or not stream then
288
+ -- Fallback: Studio tidak mendukung WebStreamClient → gunakan polling biasa
289
+ if ui then
290
+ ui:addLog("[System] WebStreamClient tidak tersedia. Menggunakan polling fallback.", Color3.fromRGB(255, 200, 100))
291
+ end
292
+ task.spawn(fallbackPolling)
293
+ return
294
+ end
295
+
296
+ currentStream = stream
297
+ isConnected = true
298
+ reconnectDelay = RECONNECT_DELAY_INITIAL
299
+
300
+ if ui then
301
+ ui:setStatus("waiting") -- Menunggu status mcpConnected dari server
302
+ ui:addLog("[System] SSE stream terbuka. Menunggu IDE Agent...", Color3.fromRGB(100, 200, 255))
303
+ end
304
+
305
+ -- Event: pesan masuk dari SSE stream
306
+ local onMessage = stream.OnMessage:Connect(function(message)
307
+ handleSSEMessage(message)
308
+ end)
309
+ table.insert(streamConnections, onMessage)
310
+
311
+ -- Event: stream ditutup (server restart / network drop)
312
+ local onClose = stream.OnClose:Connect(function()
313
+ isConnected = false
314
+ mcpAgentActive = false
315
+ currentStream = nil
316
+ streamConnections = {}
317
+ if ui then
318
+ ui:setStatus("waiting")
319
+ ui:addLog("[System] SSE stream terputus. Reconnect dalam " .. reconnectDelay .. "s...", Color3.fromRGB(255, 200, 100))
320
+ end
321
+ if not isManualDisconnect then
322
+ task.delay(reconnectDelay, connectSSE)
323
+ reconnectDelay = math.min(reconnectDelay * 2, RECONNECT_DELAY_MAX)
253
324
  end
254
- isPolling = false
255
325
  end)
326
+ table.insert(streamConnections, onClose)
256
327
  end
257
328
 
258
- -- ══════════════════════════════════════════════════════════════════════
259
- -- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
260
- -- Mendeteksi apakah server MCP aktif setiap 2 detik.
261
- -- Jika terdeteksi aktif → otomatis connect (tanpa perlu tekan tombol).
262
- -- Jika terdeteksi mati → otomatis update status.
263
- -- ══════════════════════════════════════════════════════════════════════
264
- task.spawn(function()
265
- task.wait(1) -- Tunggu sebentar agar UI siap
266
-
329
+ -- ── Fallback: HTTP Polling (jika WebStreamClient tidak tersedia) ──────────────
330
+ -- Digunakan pada versi Studio lama yang belum mendukung WebStreamClient.
331
+ local isPollingActive = false
332
+ function fallbackPolling()
333
+ if isPollingActive then return end
334
+ isPollingActive = true
335
+ isConnected = true
336
+
267
337
  if ui then
268
- ui:addLog("[System] Plugin termuat. Auto-detect server MCP aktif...", Color3.fromRGB(200, 200, 200))
338
+ ui:setStatus("waiting")
339
+ ui:addLog("[System] Mode Polling aktif (WebStreamClient tidak didukung).", Color3.fromRGB(200, 200, 100))
269
340
  end
270
-
271
- while true do
272
- -- Skip heartbeat jika polling loop sudah aktif (ia sudah menangani semuanya)
273
- if not isPolling then
274
- local success, errorMsg = pcall(function()
275
- local cacheBuster = "?t=" .. tostring(os.clock())
276
- return HttpService:GetAsync(TASK_URL .. cacheBuster)
341
+
342
+ while isConnected and not isManualDisconnect do
343
+ -- Ping
344
+ local pingOk, _ = pcall(function()
345
+ return HttpService:GetAsync(PING_URL .. "?studioId=" .. STUDIO_ID .. "&t=" .. tostring(os.clock()))
346
+ end)
347
+
348
+ if pingOk then
349
+ if ui then ui:setStatus("connected") end
350
+
351
+ -- Long-poll tasks
352
+ local taskOk, taskResponse = pcall(function()
353
+ return HttpService:GetAsync(BASE_URL .. "/api/tasks?t=" .. tostring(os.clock()))
277
354
  end)
278
-
279
- if success then
280
- -- Server terdeteksi aktif!
281
- if not isConnected and not isManualDisconnect then
282
- isConnected = true
283
- if ui then
284
- ui:setStatus("connected")
285
- ui:addLog("[System] Server MCP terdeteksi! Auto-connected.", Color3.fromRGB(100, 255, 100))
286
- end
287
- startPolling()
288
- end
289
- else
290
- -- Server tidak aktif
291
- if isConnected then
292
- -- Server baru saja mati (sebelumnya connected) — polling loop juga akan exit
293
- isConnected = false
294
- if ui then
295
- ui:setStatus("waiting")
296
- ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
297
- end
298
- elseif not isManualDisconnect then
299
- if ui then
300
- ui:setStatus("waiting")
301
- -- HANYA print error jika belum terkoneksi, untuk debugging
302
- ui:addLog("[Debug] Deteksi gagal: " .. tostring(errorMsg), Color3.fromRGB(150, 150, 150))
355
+
356
+ if taskOk and taskResponse then
357
+ local decodeOk, taskData = pcall(function() return HttpService:JSONDecode(taskResponse) end)
358
+ if decodeOk and taskData and taskData.id then
359
+ local resultData = processTask(taskData)
360
+ if resultData then
361
+ sendResponse(taskData.id, resultData)
303
362
  end
304
363
  end
305
364
  end
365
+ task.wait(0.1)
366
+ else
367
+ if ui then ui:setStatus("waiting") end
368
+ task.wait(1.5)
306
369
  end
307
-
308
- task.wait(2)
309
370
  end
371
+
372
+ isPollingActive = false
373
+ end
374
+
375
+ -- ── Plugin Unloading ──────────────────────────────────────────────────────────
376
+ plugin.Unloading:Connect(function()
377
+ isManualDisconnect = true
378
+ disconnectSSE()
379
+ end)
380
+
381
+ -- ══════════════════════════════════════════════════════════════════════
382
+ -- STARTUP: Langsung buka SSE stream
383
+ -- ══════════════════════════════════════════════════════════════════════
384
+ task.spawn(function()
385
+ task.wait(0.5) -- Tunggu UI siap
386
+
387
+ if ui then
388
+ ui:addLog("[System] NVStudio MCP v2.1.0 termuat. Membuka SSE stream...", Color3.fromRGB(200, 200, 200))
389
+ end
390
+
391
+ connectSSE()
310
392
  end)