@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.
@@ -378,179 +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 PING_URL = "http://localhost:3055/api/ping"
385
- local TASK_URL = "http://localhost:3055/api/tasks"
386
- 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"
387
396
 
388
- -- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
389
- 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)
390
399
 
391
- -- Import UI Manager (Procedural Dashboard)
400
+ -- ── UI Module ──────────────────────────────────────────────────────────────────
392
401
  local uiModule = script.Parent:FindFirstChild("ui_manager")
393
402
  local ui = nil
394
403
 
395
- -- Membangun Tombol Toolbar (Toggle)
396
- local toolbar = plugin:CreateToolbar("NVStudio MCP")
404
+ -- ── Toolbar ───────────────────────────────────────────────────────────────────
405
+ local toolbar = plugin:CreateToolbar("NVStudio MCP")
397
406
  local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722", "NVStudio")
398
407
 
399
- -- Global State & Context AI
408
+ -- ── State ─────────────────────────────────────────────────────────────────────
400
409
  local ctx = {
401
410
  scriptHistory = {},
402
- recentLogs = {},
403
- HttpService = HttpService,
404
- studioId = STUDIO_ID
411
+ recentLogs = {},
412
+ HttpService = HttpService,
413
+ studioId = STUDIO_ID,
405
414
  }
406
- local MAX_LOGS = 50
407
- local isConnected = false
408
- local savedAutoConnect = plugin:GetSetting("AutoConnect")
409
- local autoConnect = (savedAutoConnect == nil) and true or savedAutoConnect
410
- local isPolling = false
411
- local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
412
- local isClickDebounce = false -- Mencegah rapid double-click
413
-
414
- -- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
415
- local startPolling
416
- local stopPolling
417
-
418
- -- Inisialisasi UI jika modul ditemukan
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
440
+
441
+ -- ── Inisialisasi UI ──────────────────────────────────────────────────────────
419
442
  if uiModule then
420
443
  local UIManager = require(uiModule)
421
444
  ui = UIManager.init(plugin, STUDIO_ID)
422
-
423
- -- Sync Initial State ke UI
424
- ui:setAutoConnectState(autoConnect)
425
- if autoConnect then
426
- ui:setStatus("waiting")
427
- else
428
- ui:setStatus("disconnected")
429
- end
430
-
431
- -- Sync state tombol toolbar dengan status widget
445
+
446
+ ui:setAutoConnectState(true)
447
+ ui:setStatus("waiting")
448
+
432
449
  if ui.widget then
433
450
  toggleBtn:SetActive(ui.widget.Enabled)
434
451
  ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
435
452
  toggleBtn:SetActive(ui.widget.Enabled)
436
453
  end)
437
454
  end
438
-
439
- -- Event Toggle GUI Panel
455
+
440
456
  toggleBtn.Click:Connect(function()
441
457
  ui:toggleVisibility()
442
458
  end)
443
-
444
- -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
459
+
460
+ -- Tombol Connect / Disconnect
445
461
  ui.connectBtn.MouseButton1Click:Connect(function()
446
462
  if isClickDebounce then return end
447
463
  isClickDebounce = true
448
-
464
+
449
465
  if not isConnected then
450
- -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
451
466
  isManualDisconnect = false
467
+ reconnectDelay = RECONNECT_DELAY_INITIAL
452
468
  ui:setStatus("waiting")
453
- ui:addLog("[System] Menghubungkan ke Server MCP...", Color3.fromRGB(255, 220, 100))
454
-
455
- task.spawn(function()
456
- local cacheBuster = "?t=" .. tostring(os.clock())
457
- local success, _ = pcall(function()
458
- return HttpService:GetAsync(PING_URL .. cacheBuster)
459
- end)
460
-
461
- isConnected = true
462
- if success then
463
- if ui then
464
- ui:setStatus("connected")
465
- ui:addLog("[System] Berhasil terhubung ke Server MCP!", Color3.fromRGB(100, 255, 100))
466
- end
467
- else
468
- if ui then
469
- ui:setStatus("waiting")
470
- ui:addLog("[System] Server MCP belum aktif di port 3055. Menunggu koneksi...", Color3.fromRGB(255, 200, 100))
471
- end
472
- end
473
- startPolling()
474
- end)
469
+ ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
470
+ task.spawn(connectSSE)
475
471
  else
476
- -- DISCONNECT — hentikan polling dan tandai manual disconnect
477
- stopPolling()
472
+ isManualDisconnect = true
473
+ disconnectSSE()
478
474
  ui:setStatus("disconnected")
479
475
  ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
480
476
  end
481
-
482
- task.delay(0.3, function()
483
- isClickDebounce = false
484
- end)
485
- end)
486
-
487
- -- Event Custom Checkbox (Auto-Connect)
488
- ui.autoConnectBtn.MouseButton1Click:Connect(function()
489
- autoConnect = not autoConnect
490
- plugin:SetSetting("AutoConnect", autoConnect)
491
- ui:setAutoConnectState(autoConnect)
492
- ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
477
+
478
+ task.delay(0.3, function() isClickDebounce = false end)
493
479
  end)
494
480
  else
495
- -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
496
481
  toggleBtn.Click:Connect(function()
497
- 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!")
498
483
  end)
499
484
  end
500
485
 
501
- -- Handler Output Studio (Untuk AI dan GUI)
486
+ -- ── Log Capture ──────────────────────────────────────────────────────────────
502
487
  local function addSystemLog(msg, msgType)
503
- local color = Color3.fromRGB(200, 200, 200)
488
+ local color = Color3.fromRGB(200, 200, 200)
504
489
  local prefix = "[INFO] "
505
490
  if msgType == Enum.MessageType.MessageError then
506
- prefix = "[ERROR] "
507
- color = Color3.fromRGB(255, 100, 100)
491
+ prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
508
492
  elseif msgType == Enum.MessageType.MessageWarning then
509
- prefix = "[WARN] "
510
- color = Color3.fromRGB(255, 200, 100)
493
+ prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
511
494
  end
512
-
513
495
  local fullMsg = prefix .. msg
514
-
515
- -- 1. Simpan di memori untuk disedot AI
516
496
  table.insert(ctx.recentLogs, fullMsg)
517
- if #ctx.recentLogs > MAX_LOGS then
518
- table.remove(ctx.recentLogs, 1)
519
- end
520
-
521
- -- 2. Cetak ke Terminal GUI Plugin
522
- if ui then
523
- ui:addLog(fullMsg, color)
524
- end
497
+ if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
498
+ if ui then ui:addLog(fullMsg, color) end
525
499
  end
526
-
527
- -- Ikat listener output
528
500
  LogService.MessageOut:Connect(addSystemLog)
529
501
 
530
- -- Memuat semua module skills secara dinamis
531
- local skills = {}
532
- for _, module in ipairs(script.Parent:GetChildren()) do
533
- if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
534
- skills[module.Name] = require(module)
535
- end
536
- end
537
-
538
- -- Fungsi Helper Pathing
502
+ -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
539
503
  local function getInstanceFromPath(pathString)
540
- local parts = string.split(pathString, ".")
504
+ local parts = string.split(pathString, ".")
541
505
  local current = game
542
-
543
506
  if parts[1] == "game" then table.remove(parts, 1) end
544
-
545
- -- Handle common Services directly to ensure reliability
546
507
  pcall(function()
547
508
  local svc = game:GetService(parts[1])
548
- if svc then
549
- current = svc
550
- table.remove(parts, 1)
551
- end
509
+ if svc then current = svc; table.remove(parts, 1) end
552
510
  end)
553
-
554
511
  for _, part in ipairs(parts) do
555
512
  local found = current:FindFirstChild(part)
556
513
  if not found then return nil end
@@ -559,157 +516,259 @@ local function getInstanceFromPath(pathString)
559
516
  return current
560
517
  end
561
518
 
562
- -- Memproses Task Eksekusi dari AI
519
+ -- ── Proses Task dari Server ───────────────────────────────────────────────────
563
520
  local function processTask(taskData)
564
521
  if not taskData or not taskData.id or not taskData.command then return nil end
565
-
566
- if ui then
567
- 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))
568
525
  end
569
-
526
+
570
527
  local targetInstance = getInstanceFromPath(taskData.target)
571
528
  if not targetInstance then
572
- return {
573
- id = taskData.id,
574
- status = "error",
575
- error = "Target path tidak ditemukan: " .. taskData.target
576
- }
529
+ return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
577
530
  end
578
531
 
579
532
  local skillFunc = skills[taskData.command]
580
533
  if not skillFunc then
581
- return {
582
- id = taskData.id,
583
- status = "error",
584
- error = "Command tidak dikenali oleh Studio: " .. taskData.command
585
- }
534
+ return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
586
535
  end
587
536
 
588
- -- Eksekusi skill AI
589
537
  local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
590
- result.id = taskData.id
591
-
592
- -- Cetak hasil eksekusi ke Terminal GUI
538
+ result.id = taskData.id
539
+
593
540
  if ui then
594
541
  local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
595
542
  ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
596
543
  end
597
-
598
- return result
599
- end
600
544
 
601
- -- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
602
- function stopPolling()
603
- isConnected = false
604
- isManualDisconnect = true
605
- -- isPolling akan menjadi false secara otomatis saat loop keluar
545
+ return result
606
546
  end
607
547
 
608
- -- Background Polling Loop (Dapat dihentikan oleh user)
609
- function startPolling()
610
- if isPolling then return end
611
- isPolling = true
612
- if ui and isConnected then ui:setStatus("connected") end
613
-
548
+ -- ── Kirim Respons ke Server ───────────────────────────────────────────────────
549
+ local function sendResponse(taskId, resultData)
614
550
  task.spawn(function()
615
- while isConnected do
616
- local success, response = pcall(function()
617
- -- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
618
- local cacheBuster = "?t=" .. tostring(os.clock())
619
- 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
+ )
620
560
  end)
621
-
622
- -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
623
- if not isConnected then
624
- break
561
+ if not success then
562
+ HttpService:PostAsync(RESPONSE_URL, json, Enum.HttpContentType.ApplicationJson)
625
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
570
+
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
626
580
 
627
- if success and response then
628
- -- JIKA PING/POLL SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
629
- if ui then ui:setStatus("connected") end
581
+ local ok, decoded = pcall(function() return HttpService:JSONDecode(payload) end)
582
+ if not ok or type(decoded) ~= "table" then return end
630
583
 
631
- local decodeSuccess, taskData = pcall(function()
632
- return HttpService:JSONDecode(response)
633
- end)
634
-
635
- if decodeSuccess and taskData and taskData.id then
636
- local resultData = processTask(taskData)
637
- if resultData then
638
- pcall(function()
639
- local jsonResult = HttpService:JSONEncode(resultData)
640
- HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
641
- end)
642
- end
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))
643
604
  end
644
-
645
- -- Fast-polling (long-polling yield di server Node.js)
646
- -- 50ms untuk safety mencegah Studio Engine freeze jika Node.js membalas instant
647
- task.wait(0.05)
648
- else
649
- -- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
650
- if ui then ui:setStatus("waiting") end
651
- task.wait(1.5)
652
605
  end
653
-
654
- -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
655
- if not isConnected then
656
- 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)
657
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))
671
+ end
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)
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)
658
704
  end
659
- isPolling = false
660
705
  end)
706
+ table.insert(streamConnections, onClose)
661
707
  end
662
708
 
663
- -- ══════════════════════════════════════════════════════════════════════
664
- -- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
665
- -- Mendeteksi apakah server MCP aktif setiap 1.5 detik via PING_URL.
666
- -- Menggunakan /api/ping agar instan (tidak tertahan 20 detik oleh long-poll).
667
- -- ══════════════════════════════════════════════════════════════════════
668
- task.spawn(function()
669
- task.wait(0.5) -- Tunggu sebentar agar UI siap
670
-
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
+
671
717
  if ui then
672
- 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))
673
720
  end
674
-
675
- while true do
676
- -- Hanya jalankan auto-detect jika user tidak sengaja menekan Disconnect
677
- if not isManualDisconnect and (autoConnect or isConnected) then
678
- if not isPolling then
679
- local success, _ = pcall(function()
680
- local cacheBuster = "?t=" .. tostring(os.clock())
681
- return HttpService:GetAsync(PING_URL .. cacheBuster)
682
- end)
683
-
684
- if success then
685
- -- Server terdeteksi aktif via Ping!
686
- if not isConnected then
687
- isConnected = true
688
- if ui then
689
- ui:setStatus("connected")
690
- ui:addLog("[System] Server MCP terdeteksi! Terhubung otomatis.", Color3.fromRGB(100, 255, 100))
691
- end
692
- startPolling()
693
- else
694
- startPolling()
695
- end
696
- else
697
- -- Server tidak aktif / belum menyala
698
- if isConnected then
699
- isConnected = false
700
- if ui then
701
- ui:setStatus("waiting")
702
- ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
703
- end
704
- elseif ui and ui.status ~= "waiting" and not isManualDisconnect then
705
- ui:setStatus("waiting")
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()))
734
+ end)
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)
706
742
  end
707
743
  end
708
744
  end
745
+ task.wait(0.1)
746
+ else
747
+ if ui then ui:setStatus("waiting") end
748
+ task.wait(1.5)
709
749
  end
710
-
711
- task.wait(1.5)
712
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()
713
772
  end)
714
773
  ]]></ProtectedString>
715
774
  </Properties>