@adiholick/nvstudio-mcp 2.0.4 → 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,176 +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 PING_URL = "http://localhost:3055/api/ping"
5
- local TASK_URL = "http://localhost:3055/api/tasks"
6
- local RESPONSE_URL = "http://localhost:3055/api/response"
8
+ local HttpService = game:GetService("HttpService")
9
+ local LogService = game:GetService("LogService")
7
10
 
8
- -- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
9
- 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"
10
16
 
11
- -- 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 ──────────────────────────────────────────────────────────────────
12
21
  local uiModule = script.Parent:FindFirstChild("ui_manager")
13
22
  local ui = nil
14
23
 
15
- -- Membangun Tombol Toolbar (Toggle)
16
- local toolbar = plugin:CreateToolbar("NVStudio MCP")
24
+ -- ── Toolbar ───────────────────────────────────────────────────────────────────
25
+ local toolbar = plugin:CreateToolbar("NVStudio MCP")
17
26
  local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722", "NVStudio")
18
27
 
19
- -- Global State & Context AI
28
+ -- ── State ─────────────────────────────────────────────────────────────────────
20
29
  local ctx = {
21
30
  scriptHistory = {},
22
- recentLogs = {},
23
- HttpService = HttpService,
24
- studioId = STUDIO_ID
31
+ recentLogs = {},
32
+ HttpService = HttpService,
33
+ studioId = STUDIO_ID,
25
34
  }
26
- local MAX_LOGS = 50
27
- local isConnected = false
28
- local savedAutoConnect = plugin:GetSetting("AutoConnect")
29
- local autoConnect = (savedAutoConnect == nil) and true or savedAutoConnect
30
- local isPolling = false
31
- local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
32
- local isClickDebounce = false -- Mencegah rapid double-click
33
-
34
- -- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
35
- local startPolling
36
- local stopPolling
37
-
38
- -- Inisialisasi UI jika modul ditemukan
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
47
+
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
60
+
61
+ -- ── Inisialisasi UI ──────────────────────────────────────────────────────────
39
62
  if uiModule then
40
63
  local UIManager = require(uiModule)
41
64
  ui = UIManager.init(plugin, STUDIO_ID)
42
-
43
- -- Sync Initial State ke UI
44
- ui:setAutoConnectState(autoConnect)
45
- if autoConnect then
46
- ui:setStatus("waiting")
47
- else
48
- ui:setStatus("disconnected")
49
- end
50
-
51
- -- Sync state tombol toolbar dengan status widget
65
+
66
+ ui:setAutoConnectState(true)
67
+ ui:setStatus("waiting")
68
+
52
69
  if ui.widget then
53
70
  toggleBtn:SetActive(ui.widget.Enabled)
54
71
  ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
55
72
  toggleBtn:SetActive(ui.widget.Enabled)
56
73
  end)
57
74
  end
58
-
59
- -- Event Toggle GUI Panel
75
+
60
76
  toggleBtn.Click:Connect(function()
61
77
  ui:toggleVisibility()
62
78
  end)
63
-
64
- -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
79
+
80
+ -- Tombol Connect / Disconnect
65
81
  ui.connectBtn.MouseButton1Click:Connect(function()
66
82
  if isClickDebounce then return end
67
83
  isClickDebounce = true
68
-
84
+
69
85
  if not isConnected then
70
- -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
71
86
  isManualDisconnect = false
87
+ reconnectDelay = RECONNECT_DELAY_INITIAL
72
88
  ui:setStatus("waiting")
73
- ui:addLog("[System] Menghubungkan ke Server MCP...", Color3.fromRGB(255, 220, 100))
74
-
75
- task.spawn(function()
76
- local cacheBuster = "?t=" .. tostring(os.clock())
77
- local success, _ = pcall(function()
78
- return HttpService:GetAsync(PING_URL .. cacheBuster)
79
- end)
80
-
81
- isConnected = true
82
- if success then
83
- if ui then
84
- ui:setStatus("connected")
85
- ui:addLog("[System] Berhasil terhubung ke Server MCP!", Color3.fromRGB(100, 255, 100))
86
- end
87
- else
88
- if ui then
89
- ui:setStatus("waiting")
90
- ui:addLog("[System] Server MCP belum aktif di port 3055. Menunggu koneksi...", Color3.fromRGB(255, 200, 100))
91
- end
92
- end
93
- startPolling()
94
- end)
89
+ ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
90
+ task.spawn(connectSSE)
95
91
  else
96
- -- DISCONNECT — hentikan polling dan tandai manual disconnect
97
- stopPolling()
92
+ isManualDisconnect = true
93
+ disconnectSSE()
98
94
  ui:setStatus("disconnected")
99
95
  ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
100
96
  end
101
-
102
- task.delay(0.3, function()
103
- isClickDebounce = false
104
- end)
105
- end)
106
-
107
- -- Event Custom Checkbox (Auto-Connect)
108
- ui.autoConnectBtn.MouseButton1Click:Connect(function()
109
- autoConnect = not autoConnect
110
- plugin:SetSetting("AutoConnect", autoConnect)
111
- ui:setAutoConnectState(autoConnect)
112
- ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
97
+
98
+ task.delay(0.3, function() isClickDebounce = false end)
113
99
  end)
114
100
  else
115
- -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
116
101
  toggleBtn.Click:Connect(function()
117
- 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!")
118
103
  end)
119
104
  end
120
105
 
121
- -- Handler Output Studio (Untuk AI dan GUI)
106
+ -- ── Log Capture ──────────────────────────────────────────────────────────────
122
107
  local function addSystemLog(msg, msgType)
123
- local color = Color3.fromRGB(200, 200, 200)
108
+ local color = Color3.fromRGB(200, 200, 200)
124
109
  local prefix = "[INFO] "
125
110
  if msgType == Enum.MessageType.MessageError then
126
- prefix = "[ERROR] "
127
- color = Color3.fromRGB(255, 100, 100)
111
+ prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
128
112
  elseif msgType == Enum.MessageType.MessageWarning then
129
- prefix = "[WARN] "
130
- color = Color3.fromRGB(255, 200, 100)
113
+ prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
131
114
  end
132
-
133
115
  local fullMsg = prefix .. msg
134
-
135
- -- 1. Simpan di memori untuk disedot AI
136
116
  table.insert(ctx.recentLogs, fullMsg)
137
- if #ctx.recentLogs > MAX_LOGS then
138
- table.remove(ctx.recentLogs, 1)
139
- end
140
-
141
- -- 2. Cetak ke Terminal GUI Plugin
142
- if ui then
143
- ui:addLog(fullMsg, color)
144
- end
117
+ if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
118
+ if ui then ui:addLog(fullMsg, color) end
145
119
  end
146
-
147
- -- Ikat listener output
148
120
  LogService.MessageOut:Connect(addSystemLog)
149
121
 
150
- -- Memuat semua module skills secara dinamis
151
- local skills = {}
152
- for _, module in ipairs(script.Parent:GetChildren()) do
153
- if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
154
- skills[module.Name] = require(module)
155
- end
156
- end
157
-
158
- -- Fungsi Helper Pathing
122
+ -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
159
123
  local function getInstanceFromPath(pathString)
160
- local parts = string.split(pathString, ".")
124
+ local parts = string.split(pathString, ".")
161
125
  local current = game
162
-
163
126
  if parts[1] == "game" then table.remove(parts, 1) end
164
-
165
- -- Handle common Services directly to ensure reliability
166
127
  pcall(function()
167
128
  local svc = game:GetService(parts[1])
168
- if svc then
169
- current = svc
170
- table.remove(parts, 1)
171
- end
129
+ if svc then current = svc; table.remove(parts, 1) end
172
130
  end)
173
-
174
131
  for _, part in ipairs(parts) do
175
132
  local found = current:FindFirstChild(part)
176
133
  if not found then return nil end
@@ -179,155 +136,257 @@ local function getInstanceFromPath(pathString)
179
136
  return current
180
137
  end
181
138
 
182
- -- Memproses Task Eksekusi dari AI
139
+ -- ── Proses Task dari Server ───────────────────────────────────────────────────
183
140
  local function processTask(taskData)
184
141
  if not taskData or not taskData.id or not taskData.command then return nil end
185
-
186
- if ui then
187
- 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))
188
145
  end
189
-
146
+
190
147
  local targetInstance = getInstanceFromPath(taskData.target)
191
148
  if not targetInstance then
192
- return {
193
- id = taskData.id,
194
- status = "error",
195
- error = "Target path tidak ditemukan: " .. taskData.target
196
- }
149
+ return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
197
150
  end
198
151
 
199
152
  local skillFunc = skills[taskData.command]
200
153
  if not skillFunc then
201
- return {
202
- id = taskData.id,
203
- status = "error",
204
- error = "Command tidak dikenali oleh Studio: " .. taskData.command
205
- }
154
+ return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
206
155
  end
207
156
 
208
- -- Eksekusi skill AI
209
157
  local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
210
- result.id = taskData.id
211
-
212
- -- Cetak hasil eksekusi ke Terminal GUI
158
+ result.id = taskData.id
159
+
213
160
  if ui then
214
161
  local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
215
162
  ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
216
163
  end
217
-
218
- return result
219
- end
220
164
 
221
- -- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
222
- function stopPolling()
223
- isConnected = false
224
- isManualDisconnect = true
225
- -- isPolling akan menjadi false secara otomatis saat loop keluar
165
+ return result
226
166
  end
227
167
 
228
- -- Background Polling Loop (Dapat dihentikan oleh user)
229
- function startPolling()
230
- if isPolling then return end
231
- isPolling = true
232
- if ui and isConnected then ui:setStatus("connected") end
233
-
168
+ -- ── Kirim Respons ke Server ───────────────────────────────────────────────────
169
+ local function sendResponse(taskId, resultData)
234
170
  task.spawn(function()
235
- while isConnected do
236
- local success, response = pcall(function()
237
- -- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
238
- local cacheBuster = "?t=" .. tostring(os.clock())
239
- 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
+ )
240
180
  end)
241
-
242
- -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
243
- if not isConnected then
244
- break
181
+ if not success then
182
+ HttpService:PostAsync(RESPONSE_URL, json, Enum.HttpContentType.ApplicationJson)
245
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
246
190
 
247
- if success and response then
248
- -- JIKA PING/POLL SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
249
- 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
250
200
 
251
- local decodeSuccess, taskData = pcall(function()
252
- return HttpService:JSONDecode(response)
253
- end)
254
-
255
- if decodeSuccess and taskData and taskData.id then
256
- local resultData = processTask(taskData)
257
- if resultData then
258
- pcall(function()
259
- local jsonResult = HttpService:JSONEncode(resultData)
260
- HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
261
- end)
262
- 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))
263
224
  end
264
-
265
- -- Fast-polling (long-polling yield di server Node.js)
266
- -- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
267
- task.wait(0.05)
268
- else
269
- -- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
270
- if ui then ui:setStatus("waiting") end
271
- task.wait(1.5)
272
225
  end
273
-
274
- -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
275
- if not isConnected then
276
- 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)
277
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))
278
291
  end
279
- isPolling = false
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)
280
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)
324
+ end
325
+ end)
326
+ table.insert(streamConnections, onClose)
281
327
  end
282
328
 
283
- -- ══════════════════════════════════════════════════════════════════════
284
- -- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
285
- -- Mendeteksi apakah server MCP aktif setiap 1.5 detik via PING_URL.
286
- -- Menggunakan /api/ping agar instan (tidak tertahan 20 detik oleh long-poll).
287
- -- ══════════════════════════════════════════════════════════════════════
288
- task.spawn(function()
289
- task.wait(0.5) -- Tunggu sebentar agar UI siap
290
-
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
+
291
337
  if ui then
292
- 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))
293
340
  end
294
-
295
- while true do
296
- -- Hanya jalankan auto-detect jika user tidak sengaja menekan Disconnect
297
- if not isManualDisconnect and (autoConnect or isConnected) then
298
- if not isPolling then
299
- local success, _ = pcall(function()
300
- local cacheBuster = "?t=" .. tostring(os.clock())
301
- return HttpService:GetAsync(PING_URL .. cacheBuster)
302
- end)
303
-
304
- if success then
305
- -- Server terdeteksi aktif via Ping!
306
- if not isConnected then
307
- isConnected = true
308
- if ui then
309
- ui:setStatus("connected")
310
- ui:addLog("[System] Server MCP terdeteksi! Terhubung otomatis.", Color3.fromRGB(100, 255, 100))
311
- end
312
- startPolling()
313
- else
314
- startPolling()
315
- end
316
- else
317
- -- Server tidak aktif / belum menyala
318
- if isConnected then
319
- isConnected = false
320
- if ui then
321
- ui:setStatus("waiting")
322
- ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
323
- end
324
- elseif ui and ui.status ~= "waiting" and not isManualDisconnect then
325
- ui:setStatus("waiting")
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()))
354
+ end)
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)
326
362
  end
327
363
  end
328
364
  end
365
+ task.wait(0.1)
366
+ else
367
+ if ui then ui:setStatus("waiting") end
368
+ task.wait(1.5)
329
369
  end
330
-
331
- task.wait(1.5)
332
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()
333
392
  end)