@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.
@@ -378,153 +378,136 @@ end
378
378
  <int64 name="SourceAssetId">-1</int64>
379
379
  <BinaryString name="Tags"></BinaryString>
380
380
  <token name="RunContext">0</token>
381
- <ProtectedString name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
382
- local LogService = game:GetService("LogService")
381
+ <ProtectedString name="Source"><![CDATA[-- ══════════════════════════════════════════════════════════════════════
382
+ -- NVStudio MCP Plugin v2.1.0
383
+ -- Arsitektur: SSE (Server-Sent Events) via WebStreamClient
384
+ -- Plugin membuka koneksi stream persisten ke server Node.js.
385
+ -- Server PUSH task langsung — tidak ada polling loop.
386
+ -- ══════════════════════════════════════════════════════════════════════
387
+
388
+ local HttpService = game:GetService("HttpService")
389
+ local LogService = game:GetService("LogService")
383
390
 
384
- local TASK_URL = "http://localhost:3055/api/tasks"
385
- local RESPONSE_URL = "http://localhost:3055/api/response"
391
+ -- URL Endpoints
392
+ local BASE_URL = "http://localhost:3055"
393
+ local STREAM_URL = BASE_URL .. "/api/stream"
394
+ local RESPONSE_URL = BASE_URL .. "/api/response" -- /api/response/:taskId via POST
395
+ local PING_URL = BASE_URL .. "/api/ping"
386
396
 
387
- -- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
388
- local STUDIO_ID = HttpService:GenerateGUID(false)
397
+ -- ID unik per sesi Studio (jika dua Studio terbuka, keduanya didaftarkan terpisah)
398
+ local STUDIO_ID = HttpService:GenerateGUID(false)
389
399
 
390
- -- Import UI Manager (Procedural Dashboard)
400
+ -- ── UI Module ──────────────────────────────────────────────────────────────────
391
401
  local uiModule = script.Parent:FindFirstChild("ui_manager")
392
402
  local ui = nil
393
403
 
394
- -- Membangun Tombol Toolbar (Toggle)
395
- local toolbar = plugin:CreateToolbar("NVStudio MCP")
404
+ -- ── Toolbar ───────────────────────────────────────────────────────────────────
405
+ local toolbar = plugin:CreateToolbar("NVStudio MCP")
396
406
  local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722", "NVStudio")
397
407
 
398
- -- Global State & Context AI
408
+ -- ── State ─────────────────────────────────────────────────────────────────────
399
409
  local ctx = {
400
410
  scriptHistory = {},
401
- recentLogs = {},
402
- HttpService = HttpService,
403
- studioId = STUDIO_ID
411
+ recentLogs = {},
412
+ HttpService = HttpService,
413
+ studioId = STUDIO_ID,
404
414
  }
405
- local MAX_LOGS = 50
406
- local isConnected = false
407
- local autoConnect = plugin:GetSetting("AutoConnect") or false
408
- local isPolling = false
409
- local isClickDebounce = false -- Mencegah rapid double-click
410
-
411
- -- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
412
- local startPolling
413
- local stopPolling
415
+ local MAX_LOGS = 50
416
+ local isConnected = false -- apakah SSE stream aktif
417
+ local mcpAgentActive = false -- apakah Antigravity/Cursor sedang konek ke MCP
418
+ local isManualDisconnect = false
419
+ local isClickDebounce = false
420
+ local currentStream = nil -- WebStreamClient aktif
421
+ local streamConnections = {} -- RBXScriptConnection SSE
422
+
423
+ -- Reconnect config (exponential backoff)
424
+ local RECONNECT_DELAY_INITIAL = 0.5
425
+ local RECONNECT_DELAY_MAX = 10
426
+ local reconnectDelay = RECONNECT_DELAY_INITIAL
427
+
428
+ -- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
429
+ local skills = {}
430
+ for _, module in ipairs(script.Parent:GetChildren()) do
431
+ if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
432
+ local ok, result = pcall(require, module)
433
+ if ok then
434
+ skills[module.Name] = result
435
+ else
436
+ warn("[NVStudio] Gagal load module '" .. module.Name .. "': " .. tostring(result))
437
+ end
438
+ end
439
+ end
414
440
 
415
- -- Inisialisasi UI jika modul ditemukan
441
+ -- ── Inisialisasi UI ──────────────────────────────────────────────────────────
416
442
  if uiModule then
417
443
  local UIManager = require(uiModule)
418
444
  ui = UIManager.init(plugin, STUDIO_ID)
419
-
420
- -- Sync Initial State ke UI
421
- ui:setAutoConnectState(autoConnect)
422
- ui:setStatus("disconnected")
423
-
424
- -- Sync state tombol toolbar dengan status widget
445
+
446
+ ui:setAutoConnectState(true)
447
+ ui:setStatus("waiting")
448
+
425
449
  if ui.widget then
426
450
  toggleBtn:SetActive(ui.widget.Enabled)
427
451
  ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
428
452
  toggleBtn:SetActive(ui.widget.Enabled)
429
453
  end)
430
454
  end
431
-
432
- -- Event Toggle GUI Panel
455
+
433
456
  toggleBtn.Click:Connect(function()
434
457
  ui:toggleVisibility()
435
458
  end)
436
-
437
- -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
459
+
460
+ -- Tombol Connect / Disconnect
438
461
  ui.connectBtn.MouseButton1Click:Connect(function()
439
462
  if isClickDebounce then return end
440
463
  isClickDebounce = true
441
-
464
+
442
465
  if not isConnected then
443
- -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
444
466
  isManualDisconnect = false
445
- isConnected = true
467
+ reconnectDelay = RECONNECT_DELAY_INITIAL
446
468
  ui:setStatus("waiting")
447
- ui:addLog("[System] Koneksi manual dimulai. Mencari AI Server (Waiting)...", Color3.fromRGB(255, 220, 100))
448
- startPolling()
469
+ ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
470
+ task.spawn(connectSSE)
449
471
  else
450
- -- DISCONNECT — hentikan polling dan tandai manual disconnect
451
- stopPolling()
472
+ isManualDisconnect = true
473
+ disconnectSSE()
452
474
  ui:setStatus("disconnected")
453
475
  ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
454
476
  end
455
-
456
- task.delay(0.3, function()
457
- isClickDebounce = false
458
- end)
459
- end)
460
-
461
- -- Event Custom Checkbox (Auto-Connect)
462
- ui.autoConnectBtn.MouseButton1Click:Connect(function()
463
- autoConnect = not autoConnect
464
- plugin:SetSetting("AutoConnect", autoConnect)
465
- ui:setAutoConnectState(autoConnect)
466
- ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
477
+
478
+ task.delay(0.3, function() isClickDebounce = false end)
467
479
  end)
468
480
  else
469
- -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
470
481
  toggleBtn.Click:Connect(function()
471
- print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan di dalam Folder. GUI Gagal dimuat!")
482
+ print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan. GUI gagal dimuat!")
472
483
  end)
473
484
  end
474
485
 
475
- -- Handler Output Studio (Untuk AI dan GUI)
486
+ -- ── Log Capture ──────────────────────────────────────────────────────────────
476
487
  local function addSystemLog(msg, msgType)
477
- local color = Color3.fromRGB(200, 200, 200)
488
+ local color = Color3.fromRGB(200, 200, 200)
478
489
  local prefix = "[INFO] "
479
490
  if msgType == Enum.MessageType.MessageError then
480
- prefix = "[ERROR] "
481
- color = Color3.fromRGB(255, 100, 100)
491
+ prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
482
492
  elseif msgType == Enum.MessageType.MessageWarning then
483
- prefix = "[WARN] "
484
- color = Color3.fromRGB(255, 200, 100)
493
+ prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
485
494
  end
486
-
487
495
  local fullMsg = prefix .. msg
488
-
489
- -- 1. Simpan di memori untuk disedot AI
490
496
  table.insert(ctx.recentLogs, fullMsg)
491
- if #ctx.recentLogs > MAX_LOGS then
492
- table.remove(ctx.recentLogs, 1)
493
- end
494
-
495
- -- 2. Cetak ke Terminal GUI Plugin
496
- if ui then
497
- ui:addLog(fullMsg, color)
498
- end
497
+ if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
498
+ if ui then ui:addLog(fullMsg, color) end
499
499
  end
500
-
501
- -- Ikat listener output
502
500
  LogService.MessageOut:Connect(addSystemLog)
503
501
 
504
- -- Memuat semua module skills secara dinamis
505
- local skills = {}
506
- for _, module in ipairs(script.Parent:GetChildren()) do
507
- if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
508
- skills[module.Name] = require(module)
509
- end
510
- end
511
-
512
- -- Fungsi Helper Pathing
502
+ -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
513
503
  local function getInstanceFromPath(pathString)
514
- local parts = string.split(pathString, ".")
504
+ local parts = string.split(pathString, ".")
515
505
  local current = game
516
-
517
506
  if parts[1] == "game" then table.remove(parts, 1) end
518
-
519
- -- Handle common Services directly to ensure reliability
520
507
  pcall(function()
521
508
  local svc = game:GetService(parts[1])
522
- if svc then
523
- current = svc
524
- table.remove(parts, 1)
525
- end
509
+ if svc then current = svc; table.remove(parts, 1) end
526
510
  end)
527
-
528
511
  for _, part in ipairs(parts) do
529
512
  local found = current:FindFirstChild(part)
530
513
  if not found then return nil end
@@ -533,160 +516,259 @@ local function getInstanceFromPath(pathString)
533
516
  return current
534
517
  end
535
518
 
536
- -- Memproses Task Eksekusi dari AI
519
+ -- ── Proses Task dari Server ───────────────────────────────────────────────────
537
520
  local function processTask(taskData)
538
521
  if not taskData or not taskData.id or not taskData.command then return nil end
539
-
540
- if ui then
541
- ui:addLog("[AI Command] Menerima instruksi: " .. taskData.command, Color3.fromRGB(150, 200, 255))
522
+
523
+ if ui then
524
+ ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
542
525
  end
543
-
526
+
544
527
  local targetInstance = getInstanceFromPath(taskData.target)
545
528
  if not targetInstance then
546
- return {
547
- id = taskData.id,
548
- status = "error",
549
- error = "Target path tidak ditemukan: " .. taskData.target
550
- }
529
+ return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
551
530
  end
552
531
 
553
532
  local skillFunc = skills[taskData.command]
554
533
  if not skillFunc then
555
- return {
556
- id = taskData.id,
557
- status = "error",
558
- error = "Command tidak dikenali oleh Studio: " .. taskData.command
559
- }
534
+ return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
560
535
  end
561
536
 
562
- -- Eksekusi skill AI
563
537
  local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
564
- result.id = taskData.id
565
-
566
- -- Cetak hasil eksekusi ke Terminal GUI
538
+ result.id = taskData.id
539
+
567
540
  if ui then
568
541
  local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
569
542
  ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
570
543
  end
571
-
572
- return result
573
- end
574
544
 
575
- -- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
576
- local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
577
-
578
- function stopPolling()
579
- isConnected = false
580
- isManualDisconnect = true
581
- -- isPolling akan menjadi false secara otomatis saat loop keluar
545
+ return result
582
546
  end
583
547
 
584
- -- Background Polling Loop (Dapat dihentikan oleh user)
585
- function startPolling()
586
- if isPolling then return end
587
- isPolling = true
588
-
548
+ -- ── Kirim Respons ke Server ───────────────────────────────────────────────────
549
+ local function sendResponse(taskId, resultData)
589
550
  task.spawn(function()
590
- while isConnected do
591
- local success, response = pcall(function()
592
- -- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
593
- local cacheBuster = "?t=" .. tostring(os.clock())
594
- return HttpService:GetAsync(TASK_URL .. cacheBuster)
551
+ local ok, err = pcall(function()
552
+ local json = HttpService:JSONEncode(resultData)
553
+ -- Coba endpoint baru dulu (/api/response/:taskId), fallback ke lama
554
+ local success = pcall(function()
555
+ HttpService:PostAsync(
556
+ RESPONSE_URL .. "/" .. taskId,
557
+ json,
558
+ Enum.HttpContentType.ApplicationJson
559
+ )
595
560
  end)
596
-
597
- -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
598
- if not isConnected then
599
- break
561
+ if not success then
562
+ HttpService:PostAsync(RESPONSE_URL, json, Enum.HttpContentType.ApplicationJson)
600
563
  end
564
+ end)
565
+ if not ok and ui then
566
+ ui:addLog("[System] Gagal kirim response: " .. tostring(err), Color3.fromRGB(255, 100, 100))
567
+ end
568
+ end)
569
+ end
601
570
 
602
- if success and response then
603
- -- JIKA PING SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
604
- if ui then ui:setStatus("connected") end
571
+ -- ── Proses satu baris data SSE ────────────────────────────────────────────────
572
+ local function handleSSEMessage(rawMessage)
573
+ -- Strip prefix "data: " jika ada (beberapa Studio version memberikannya)
574
+ local payload = rawMessage
575
+ local normalized = rawMessage:gsub("\r\n", "\n"):gsub("\r", "\n")
576
+ if normalized:sub(1, 5) == "data:" then
577
+ payload = normalized:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
578
+ end
579
+ if payload == "" then return end
605
580
 
606
- local decodeSuccess, taskData = pcall(function()
607
- return HttpService:JSONDecode(response)
608
- end)
609
-
610
- if decodeSuccess and taskData and taskData.id then
611
- local resultData = processTask(taskData)
612
- if resultData then
613
- pcall(function()
614
- local jsonResult = HttpService:JSONEncode(resultData)
615
- HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
616
- end)
617
- end
581
+ local ok, decoded = pcall(function() return HttpService:JSONDecode(payload) end)
582
+ if not ok or type(decoded) ~= "table" then return end
583
+
584
+ local kind = decoded.kind
585
+
586
+ -- ── heartbeat ────────────────────────────────────────────────────
587
+ if kind == "heartbeat" then
588
+ -- Server masih hidup, tidak perlu tindakan khusus
589
+ return
590
+ end
591
+
592
+ -- ── status ───────────────────────────────────────────────────────
593
+ if kind == "status" then
594
+ local newMcpActive = decoded.mcpConnected == true
595
+ if newMcpActive ~= mcpAgentActive then
596
+ mcpAgentActive = newMcpActive
597
+ if ui then
598
+ if mcpAgentActive then
599
+ ui:setStatus("connected")
600
+ ui:addLog("[System] IDE Agent (Antigravity/Cursor) terhubung ke MCP.", Color3.fromRGB(100, 255, 100))
601
+ else
602
+ ui:setStatus("waiting")
603
+ ui:addLog("[System] IDE Agent terputus dari MCP.", Color3.fromRGB(255, 200, 100))
618
604
  end
619
-
620
- -- Fast-polling (long-polling yield di server Node.js)
621
- -- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
622
- task.wait(0.05)
623
- else
624
- -- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
625
- if ui then ui:setStatus("waiting") end
626
- task.wait(1)
627
605
  end
628
-
629
- -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
630
- if not isConnected then
631
- break
606
+ end
607
+ return
608
+ end
609
+
610
+ -- ── request (task dari AI agent) ─────────────────────────────────
611
+ if kind == "request" then
612
+ local taskId = decoded.requestId
613
+ local command = decoded.command
614
+ local target = decoded.target
615
+ local data = decoded.data
616
+
617
+ if not taskId or not command then return end
618
+
619
+ -- Proses task di coroutine terpisah agar tidak memblokir stream
620
+ task.spawn(function()
621
+ local resultData = processTask({
622
+ id = taskId,
623
+ command = command,
624
+ target = target or "",
625
+ data = data,
626
+ })
627
+ if resultData then
628
+ sendResponse(taskId, resultData)
632
629
  end
630
+ end)
631
+ return
632
+ end
633
+ end
634
+
635
+ -- ── Tutup SSE stream yang aktif ───────────────────────────────────────────────
636
+ function disconnectSSE()
637
+ isConnected = false
638
+ for _, conn in ipairs(streamConnections) do
639
+ pcall(function() conn:Disconnect() end)
640
+ end
641
+ streamConnections = {}
642
+ if currentStream then
643
+ pcall(function() currentStream:Close() end)
644
+ currentStream = nil
645
+ end
646
+ end
647
+
648
+ -- ── Buka SSE stream baru ──────────────────────────────────────────────────────
649
+ function connectSSE()
650
+ if isConnected then return end
651
+ disconnectSSE() -- Bersihkan sisa koneksi lama
652
+
653
+ local url = STREAM_URL .. "?studioId=" .. HttpService:UrlEncode(STUDIO_ID)
654
+
655
+ local ok, streamOrErr = pcall(function()
656
+ return HttpService:GetAsync(url, false) -- Gunakan non-cached request
657
+ end)
658
+
659
+ -- WebStreamClient hanya tersedia di Studio; GetAsync biasa tidak akan
660
+ -- mempertahankan koneksi. Kita butuh WebStreamClient khusus.
661
+ -- Coba via WebStreamClient API:
662
+ local streamOk, stream = pcall(function()
663
+ -- @ts-ignore – WebStreamClient mungkin belum ada di semua versi Studio
664
+ return (HttpService :: any):WebStream(url)
665
+ end)
666
+
667
+ if not streamOk or not stream then
668
+ -- Fallback: Studio tidak mendukung WebStreamClient → gunakan polling biasa
669
+ if ui then
670
+ ui:addLog("[System] WebStreamClient tidak tersedia. Menggunakan polling fallback.", Color3.fromRGB(255, 200, 100))
633
671
  end
634
- isPolling = false
672
+ task.spawn(fallbackPolling)
673
+ return
674
+ end
675
+
676
+ currentStream = stream
677
+ isConnected = true
678
+ reconnectDelay = RECONNECT_DELAY_INITIAL
679
+
680
+ if ui then
681
+ ui:setStatus("waiting") -- Menunggu status mcpConnected dari server
682
+ ui:addLog("[System] SSE stream terbuka. Menunggu IDE Agent...", Color3.fromRGB(100, 200, 255))
683
+ end
684
+
685
+ -- Event: pesan masuk dari SSE stream
686
+ local onMessage = stream.OnMessage:Connect(function(message)
687
+ handleSSEMessage(message)
635
688
  end)
689
+ table.insert(streamConnections, onMessage)
690
+
691
+ -- Event: stream ditutup (server restart / network drop)
692
+ local onClose = stream.OnClose:Connect(function()
693
+ isConnected = false
694
+ mcpAgentActive = false
695
+ currentStream = nil
696
+ streamConnections = {}
697
+ if ui then
698
+ ui:setStatus("waiting")
699
+ ui:addLog("[System] SSE stream terputus. Reconnect dalam " .. reconnectDelay .. "s...", Color3.fromRGB(255, 200, 100))
700
+ end
701
+ if not isManualDisconnect then
702
+ task.delay(reconnectDelay, connectSSE)
703
+ reconnectDelay = math.min(reconnectDelay * 2, RECONNECT_DELAY_MAX)
704
+ end
705
+ end)
706
+ table.insert(streamConnections, onClose)
636
707
  end
637
708
 
638
- -- ══════════════════════════════════════════════════════════════════════
639
- -- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
640
- -- Mendeteksi apakah server MCP aktif setiap 2 detik.
641
- -- Jika terdeteksi aktif → otomatis connect (tanpa perlu tekan tombol).
642
- -- Jika terdeteksi mati → otomatis update status.
643
- -- ══════════════════════════════════════════════════════════════════════
644
- task.spawn(function()
645
- task.wait(1) -- Tunggu sebentar agar UI siap
646
-
709
+ -- ── Fallback: HTTP Polling (jika WebStreamClient tidak tersedia) ──────────────
710
+ -- Digunakan pada versi Studio lama yang belum mendukung WebStreamClient.
711
+ local isPollingActive = false
712
+ function fallbackPolling()
713
+ if isPollingActive then return end
714
+ isPollingActive = true
715
+ isConnected = true
716
+
647
717
  if ui then
648
- ui:addLog("[System] Plugin termuat. Auto-detect server MCP aktif...", Color3.fromRGB(200, 200, 200))
718
+ ui:setStatus("waiting")
719
+ ui:addLog("[System] Mode Polling aktif (WebStreamClient tidak didukung).", Color3.fromRGB(200, 200, 100))
649
720
  end
650
-
651
- while true do
652
- -- Skip heartbeat jika polling loop sudah aktif (ia sudah menangani semuanya)
653
- if not isPolling then
654
- local success, errorMsg = pcall(function()
655
- local cacheBuster = "?t=" .. tostring(os.clock())
656
- return HttpService:GetAsync(TASK_URL .. cacheBuster)
721
+
722
+ while isConnected and not isManualDisconnect do
723
+ -- Ping
724
+ local pingOk, _ = pcall(function()
725
+ return HttpService:GetAsync(PING_URL .. "?studioId=" .. STUDIO_ID .. "&t=" .. tostring(os.clock()))
726
+ end)
727
+
728
+ if pingOk then
729
+ if ui then ui:setStatus("connected") end
730
+
731
+ -- Long-poll tasks
732
+ local taskOk, taskResponse = pcall(function()
733
+ return HttpService:GetAsync(BASE_URL .. "/api/tasks?t=" .. tostring(os.clock()))
657
734
  end)
658
-
659
- if success then
660
- -- Server terdeteksi aktif!
661
- if not isConnected and not isManualDisconnect then
662
- isConnected = true
663
- if ui then
664
- ui:setStatus("connected")
665
- ui:addLog("[System] Server MCP terdeteksi! Auto-connected.", Color3.fromRGB(100, 255, 100))
666
- end
667
- startPolling()
668
- end
669
- else
670
- -- Server tidak aktif
671
- if isConnected then
672
- -- Server baru saja mati (sebelumnya connected) — polling loop juga akan exit
673
- isConnected = false
674
- if ui then
675
- ui:setStatus("waiting")
676
- ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
677
- end
678
- elseif not isManualDisconnect then
679
- if ui then
680
- ui:setStatus("waiting")
681
- -- HANYA print error jika belum terkoneksi, untuk debugging
682
- ui:addLog("[Debug] Deteksi gagal: " .. tostring(errorMsg), Color3.fromRGB(150, 150, 150))
735
+
736
+ if taskOk and taskResponse then
737
+ local decodeOk, taskData = pcall(function() return HttpService:JSONDecode(taskResponse) end)
738
+ if decodeOk and taskData and taskData.id then
739
+ local resultData = processTask(taskData)
740
+ if resultData then
741
+ sendResponse(taskData.id, resultData)
683
742
  end
684
743
  end
685
744
  end
745
+ task.wait(0.1)
746
+ else
747
+ if ui then ui:setStatus("waiting") end
748
+ task.wait(1.5)
686
749
  end
687
-
688
- task.wait(2)
689
750
  end
751
+
752
+ isPollingActive = false
753
+ end
754
+
755
+ -- ── Plugin Unloading ──────────────────────────────────────────────────────────
756
+ plugin.Unloading:Connect(function()
757
+ isManualDisconnect = true
758
+ disconnectSSE()
759
+ end)
760
+
761
+ -- ══════════════════════════════════════════════════════════════════════
762
+ -- STARTUP: Langsung buka SSE stream
763
+ -- ══════════════════════════════════════════════════════════════════════
764
+ task.spawn(function()
765
+ task.wait(0.5) -- Tunggu UI siap
766
+
767
+ if ui then
768
+ ui:addLog("[System] NVStudio MCP v2.1.0 termuat. Membuka SSE stream...", Color3.fromRGB(200, 200, 200))
769
+ end
770
+
771
+ connectSSE()
690
772
  end)
691
773
  ]]></ProtectedString>
692
774
  </Properties>