@adiholick/nvstudio-mcp 2.0.4 → 2.1.1

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,338 +378,386 @@ 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 via Long-Poll (Roblox tidak punya native SSE client)
384
+ -- Plugin auto-connect saat dimuat, polling /api/ping lalu /api/stream
385
+ -- untuk mendapatkan events (status, request) dari server Node.js.
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" -- SSE long-poll (blocking GET)
394
+ local RESPONSE_URL = BASE_URL .. "/api/response"
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
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
- scriptHistory = {},
402
- recentLogs = {},
403
- HttpService = HttpService,
404
- studioId = STUDIO_ID
410
+ scriptHistory = {},
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 isRunning = false -- apakah loop aktif
417
+ local isManualDisconnect = false
418
+ local isClickDebounce = false
419
+ local mcpAgentActive = false -- apakah Antigravity sedang konek ke MCP
420
+ local serverReachable = false -- apakah server Node.js reachable
421
+
422
+ -- Reconnect config
423
+ local RECONNECT_DELAY_INITIAL = 1
424
+ local RECONNECT_DELAY_MAX = 15
425
+ local reconnectDelay = RECONNECT_DELAY_INITIAL
426
+
427
+ -- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
428
+ local skills = {}
429
+ for _, module in ipairs(script.Parent:GetChildren()) do
430
+ if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
431
+ local ok, result = pcall(require, module)
432
+ if ok then
433
+ skills[module.Name] = result
434
+ else
435
+ warn("[NVStudio] Gagal load module '" .. module.Name .. "': " .. tostring(result))
436
+ end
437
+ end
438
+ end
439
+
440
+ -- ── Inisialisasi UI ──────────────────────────────────────────────────────────
419
441
  if uiModule then
420
- local UIManager = require(uiModule)
421
- 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
432
- if ui.widget then
433
- toggleBtn:SetActive(ui.widget.Enabled)
434
- ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
435
- toggleBtn:SetActive(ui.widget.Enabled)
436
- end)
437
- end
438
-
439
- -- Event Toggle GUI Panel
440
- toggleBtn.Click:Connect(function()
441
- ui:toggleVisibility()
442
- end)
443
-
444
- -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
445
- ui.connectBtn.MouseButton1Click:Connect(function()
446
- if isClickDebounce then return end
447
- isClickDebounce = true
448
-
449
- if not isConnected then
450
- -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
451
- isManualDisconnect = false
452
- 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)
475
- else
476
- -- DISCONNECT — hentikan polling dan tandai manual disconnect
477
- stopPolling()
478
- ui:setStatus("disconnected")
479
- ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
480
- 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))
493
- end)
442
+ local UIManager = require(uiModule)
443
+ ui = UIManager.init(plugin, STUDIO_ID)
444
+
445
+ ui:setAutoConnectState(true)
446
+ ui:setStatus("waiting")
447
+
448
+ if ui.widget then
449
+ toggleBtn:SetActive(ui.widget.Enabled)
450
+ ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
451
+ toggleBtn:SetActive(ui.widget.Enabled)
452
+ end)
453
+ end
454
+
455
+ toggleBtn.Click:Connect(function()
456
+ ui:toggleVisibility()
457
+ end)
458
+
459
+ -- Tombol Connect / Disconnect
460
+ ui.connectBtn.MouseButton1Click:Connect(function()
461
+ if isClickDebounce then return end
462
+ isClickDebounce = true
463
+
464
+ if not isRunning then
465
+ isManualDisconnect = false
466
+ reconnectDelay = RECONNECT_DELAY_INITIAL
467
+ ui:setStatus("waiting")
468
+ ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
469
+ task.spawn(startConnectionLoop)
470
+ else
471
+ isManualDisconnect = true
472
+ isRunning = false
473
+ serverReachable = false
474
+ mcpAgentActive = false
475
+ ui:setStatus("disconnected")
476
+ ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
477
+ end
478
+
479
+ task.delay(0.3, function() isClickDebounce = false end)
480
+ end)
494
481
  else
495
- -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
496
- toggleBtn.Click:Connect(function()
497
- print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan di dalam Folder. GUI Gagal dimuat!")
498
- end)
482
+ toggleBtn.Click:Connect(function()
483
+ print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan. GUI gagal dimuat!")
484
+ end)
499
485
  end
500
486
 
501
- -- Handler Output Studio (Untuk AI dan GUI)
487
+ -- ── Log Capture ──────────────────────────────────────────────────────────────
502
488
  local function addSystemLog(msg, msgType)
503
- local color = Color3.fromRGB(200, 200, 200)
504
- local prefix = "[INFO] "
505
- if msgType == Enum.MessageType.MessageError then
506
- prefix = "[ERROR] "
507
- color = Color3.fromRGB(255, 100, 100)
508
- elseif msgType == Enum.MessageType.MessageWarning then
509
- prefix = "[WARN] "
510
- color = Color3.fromRGB(255, 200, 100)
511
- end
512
-
513
- local fullMsg = prefix .. msg
514
-
515
- -- 1. Simpan di memori untuk disedot AI
516
- 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
489
+ local color = Color3.fromRGB(200, 200, 200)
490
+ local prefix = "[INFO] "
491
+ if msgType == Enum.MessageType.MessageError then
492
+ prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
493
+ elseif msgType == Enum.MessageType.MessageWarning then
494
+ prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
495
+ end
496
+ local fullMsg = prefix .. msg
497
+ table.insert(ctx.recentLogs, fullMsg)
498
+ if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
499
+ if ui then ui:addLog(fullMsg, color) end
525
500
  end
526
-
527
- -- Ikat listener output
528
501
  LogService.MessageOut:Connect(addSystemLog)
529
502
 
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
503
+ -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
539
504
  local function getInstanceFromPath(pathString)
540
- local parts = string.split(pathString, ".")
541
- local current = game
542
-
543
- if parts[1] == "game" then table.remove(parts, 1) end
544
-
545
- -- Handle common Services directly to ensure reliability
546
- pcall(function()
547
- local svc = game:GetService(parts[1])
548
- if svc then
549
- current = svc
550
- table.remove(parts, 1)
551
- end
552
- end)
553
-
554
- for _, part in ipairs(parts) do
555
- local found = current:FindFirstChild(part)
556
- if not found then return nil end
557
- current = found
558
- end
559
- return current
505
+ local parts = string.split(pathString, ".")
506
+ local current = game
507
+ if parts[1] == "game" then table.remove(parts, 1) end
508
+ pcall(function()
509
+ local svc = game:GetService(parts[1])
510
+ if svc then current = svc; table.remove(parts, 1) end
511
+ end)
512
+ for _, part in ipairs(parts) do
513
+ local found = current:FindFirstChild(part)
514
+ if not found then return nil end
515
+ current = found
516
+ end
517
+ return current
560
518
  end
561
519
 
562
- -- Memproses Task Eksekusi dari AI
520
+ -- ── Proses Task dari Server ───────────────────────────────────────────────────
563
521
  local function processTask(taskData)
564
- 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))
568
- end
569
-
570
- local targetInstance = getInstanceFromPath(taskData.target)
571
- if not targetInstance then
572
- return {
573
- id = taskData.id,
574
- status = "error",
575
- error = "Target path tidak ditemukan: " .. taskData.target
576
- }
577
- end
522
+ if not taskData or not taskData.id or not taskData.command then return nil end
578
523
 
579
- local skillFunc = skills[taskData.command]
580
- if not skillFunc then
581
- return {
582
- id = taskData.id,
583
- status = "error",
584
- error = "Command tidak dikenali oleh Studio: " .. taskData.command
585
- }
586
- end
524
+ if ui then
525
+ ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
526
+ end
587
527
 
588
- -- Eksekusi skill AI
589
- local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
590
- result.id = taskData.id
591
-
592
- -- Cetak hasil eksekusi ke Terminal GUI
593
- if ui then
594
- local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
595
- ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
596
- end
597
-
598
- return result
528
+ local targetInstance = getInstanceFromPath(taskData.target)
529
+ if not targetInstance then
530
+ return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
531
+ end
532
+
533
+ local skillFunc = skills[taskData.command]
534
+ if not skillFunc then
535
+ return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
536
+ end
537
+
538
+ local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
539
+ result.id = taskData.id
540
+
541
+ if ui then
542
+ local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
543
+ ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
544
+ end
545
+
546
+ return result
599
547
  end
600
548
 
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
549
+ -- ── Kirim Respons ke Server ───────────────────────────────────────────────────
550
+ local function sendResponse(taskId, resultData)
551
+ task.spawn(function()
552
+ local ok, err = pcall(function()
553
+ local json = HttpService:JSONEncode(resultData)
554
+ local success = pcall(function()
555
+ HttpService:PostAsync(
556
+ RESPONSE_URL .. "/" .. taskId,
557
+ json,
558
+ Enum.HttpContentType.ApplicationJson
559
+ )
560
+ end)
561
+ if not success then
562
+ HttpService:PostAsync(RESPONSE_URL, json, Enum.HttpContentType.ApplicationJson)
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)
606
569
  end
607
570
 
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
-
614
- 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)
620
- end)
621
-
622
- -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
623
- if not isConnected then
624
- break
625
- end
571
+ -- ── Update UI berdasarkan state ───────────────────────────────────────────────
572
+ local function updateUIStatus()
573
+ if not ui then return end
574
+ if not serverReachable then
575
+ ui:setStatus("waiting")
576
+ elseif mcpAgentActive then
577
+ ui:setStatus("connected")
578
+ else
579
+ -- Server reachable tapi tidak ada AI agent → "server connected, waiting for agent"
580
+ -- Gunakan status "waiting" dengan pesan yang berbeda
581
+ ui:setStatus("waiting")
582
+ end
583
+ end
626
584
 
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
585
+ -- ── Proses satu event dari SSE stream ────────────────────────────────────────
586
+ local function handleSSELine(line)
587
+ -- Format SSE: "data: {...json...}"
588
+ local payload = line
589
+ if line:sub(1, 5) == "data:" then
590
+ payload = line:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
591
+ end
592
+ if payload == "" then return end
593
+
594
+ local ok, decoded = pcall(function() return HttpService:JSONDecode(payload) end)
595
+ if not ok or type(decoded) ~= "table" then return end
596
+
597
+ local kind = decoded.kind
598
+
599
+ -- heartbeat: server masih hidup
600
+ if kind == "heartbeat" then
601
+ return
602
+ end
603
+
604
+ -- status: update mcpConnected
605
+ if kind == "status" then
606
+ local newActive = decoded.mcpConnected == true
607
+ if newActive ~= mcpAgentActive then
608
+ mcpAgentActive = newActive
609
+ if ui then
610
+ if mcpAgentActive then
611
+ ui:setStatus("connected")
612
+ ui:addLog("[System] ✅ IDE Agent (Antigravity) terhubung ke MCP.", Color3.fromRGB(100, 255, 100))
613
+ else
614
+ ui:setStatus("waiting")
615
+ ui:addLog("[System] IDE Agent terputus dari MCP. Menunggu...", Color3.fromRGB(255, 200, 100))
616
+ end
617
+ end
618
+ end
619
+ return
620
+ end
621
+
622
+ -- request: ada task dari AI agent
623
+ if kind == "request" then
624
+ local taskId = decoded.requestId
625
+ local command = decoded.command
626
+ local target = decoded.target
627
+ local data = decoded.data
628
+
629
+ if not taskId or not command then return end
630
+
631
+ task.spawn(function()
632
+ local resultData = processTask({
633
+ id = taskId,
634
+ command = command,
635
+ target = target or "",
636
+ data = data,
637
+ })
638
+ if resultData then
639
+ sendResponse(taskId, resultData)
640
+ end
641
+ end)
642
+ return
643
+ end
644
+ end
630
645
 
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
643
- 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
- end
653
-
654
- -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
655
- if not isConnected then
656
- break
657
- end
658
- end
659
- isPolling = false
660
- end)
646
+ -- ── Loop Utama: Ping → Stream → Parse SSE lines ───────────────────────────────
647
+ -- Roblox HttpService:GetAsync() tidak mendukung streaming, jadi kita
648
+ -- menggunakan strategi: GET /api/stream dengan waktu hold singkat (5 detik).
649
+ -- Server mengirim semua events yang tertunda lalu tutup koneksi.
650
+ -- Plugin reconnect langsung untuk sesi berikutnya.
651
+ function startConnectionLoop()
652
+ if isRunning then return end
653
+ isRunning = true
654
+ reconnectDelay = RECONNECT_DELAY_INITIAL
655
+
656
+ if ui then
657
+ ui:addLog("[System] NVStudio MCP v2.1.0 — Menghubungkan ke server...", Color3.fromRGB(200, 200, 200))
658
+ end
659
+
660
+ while isRunning and not isManualDisconnect do
661
+ -- Langkah 1: Ping untuk cek apakah server hidup
662
+ local pingOk, pingRaw = pcall(function()
663
+ return HttpService:GetAsync(
664
+ PING_URL .. "?studioId=" .. HttpService:UrlEncode(STUDIO_ID) .. "&t=" .. tostring(os.clock()),
665
+ false
666
+ )
667
+ end)
668
+
669
+ if not pingOk then
670
+ -- Server tidak reachable
671
+ if serverReachable then
672
+ serverReachable = false
673
+ mcpAgentActive = false
674
+ if ui then
675
+ ui:setStatus("waiting")
676
+ ui:addLog("[System] ⚠️ Server MCP tidak dapat dijangkau. Retry dalam " .. reconnectDelay .. "s...", Color3.fromRGB(255, 150, 50))
677
+ end
678
+ end
679
+ task.wait(reconnectDelay)
680
+ reconnectDelay = math.min(reconnectDelay * 2, RECONNECT_DELAY_MAX)
681
+ else
682
+ -- Server reachable → parse ping response
683
+ reconnectDelay = RECONNECT_DELAY_INITIAL
684
+
685
+ local pingData = nil
686
+ pcall(function()
687
+ pingData = HttpService:JSONDecode(pingRaw)
688
+ end)
689
+
690
+ local wasReachable = serverReachable
691
+ serverReachable = true
692
+
693
+ if not wasReachable and ui then
694
+ ui:addLog("[System] ✅ Server MCP terhubung. Membuka event stream...", Color3.fromRGB(100, 255, 150))
695
+ end
696
+
697
+ -- Update mcpConnected dari ping response
698
+ if pingData then
699
+ local newActive = pingData.mcpConnected == true
700
+ if newActive ~= mcpAgentActive then
701
+ mcpAgentActive = newActive
702
+ if ui then
703
+ if mcpAgentActive then
704
+ ui:setStatus("connected")
705
+ ui:addLog("[System] ✅ IDE Agent (Antigravity) terhubung ke MCP.", Color3.fromRGB(100, 255, 100))
706
+ else
707
+ ui:setStatus("server_connected")
708
+ ui:addLog("[System] Menunggu IDE Agent (Antigravity/Cursor)...", Color3.fromRGB(100, 180, 255))
709
+ end
710
+ end
711
+ elseif serverReachable and not mcpAgentActive then
712
+ -- Tidak ada perubahan tapi pastikan status benar
713
+ if ui then ui:setStatus("server_connected") end
714
+ end
715
+ end
716
+
717
+ -- Langkah 2: Buka SSE stream (blocking hingga server menutup koneksi / ~10 detik)
718
+ -- Server akan mengirim events yang tertunda, heartbeat, lalu tutup.
719
+ if isRunning and not isManualDisconnect then
720
+ local streamOk, streamRaw = pcall(function()
721
+ return HttpService:GetAsync(
722
+ STREAM_URL .. "?studioId=" .. HttpService:UrlEncode(STUDIO_ID),
723
+ false
724
+ )
725
+ end)
726
+
727
+ if streamOk and streamRaw and streamRaw ~= "" then
728
+ -- Parse baris-baris SSE dari response
729
+ for line in (streamRaw .. "\n"):gmatch("([^\n]*)\n") do
730
+ line = line:gsub("\r", "")
731
+ if line ~= "" then
732
+ handleSSELine(line)
733
+ end
734
+ end
735
+ end
736
+ end
737
+
738
+ -- Jeda singkat sebelum cycle berikutnya
739
+ task.wait(0.5)
740
+ end
741
+ end
742
+
743
+ isRunning = false
744
+ serverReachable = false
745
+ mcpAgentActive = false
746
+ if ui then ui:setStatus("disconnected") end
661
747
  end
662
748
 
749
+ -- ── Plugin Unloading ──────────────────────────────────────────────────────────
750
+ plugin.Unloading:Connect(function()
751
+ isManualDisconnect = true
752
+ isRunning = false
753
+ end)
754
+
663
755
  -- ══════════════════════════════════════════════════════════════════════
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).
756
+ -- STARTUP: Langsung mulai connection loop
667
757
  -- ══════════════════════════════════════════════════════════════════════
668
758
  task.spawn(function()
669
- task.wait(0.5) -- Tunggu sebentar agar UI siap
670
-
671
- if ui then
672
- ui:addLog("[System] Plugin termuat. Auto-detect server MCP aktif...", Color3.fromRGB(200, 200, 200))
673
- 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")
706
- end
707
- end
708
- end
709
- end
710
-
711
- task.wait(1.5)
712
- end
759
+ task.wait(0.5) -- Tunggu UI siap
760
+ startConnectionLoop()
713
761
  end)
714
762
  ]]></ProtectedString>
715
763
  </Properties>
@@ -1368,7 +1416,7 @@ function UIManager:addLog(text, color)
1368
1416
  end
1369
1417
 
1370
1418
  -- ═══════════════════════════════════════════
1371
- -- 3 State Setter (Disconnected, Waiting, Connected)
1419
+ -- 4 State Setter (Disconnected, Waiting, Server Connected, Connected)
1372
1420
  -- ═══════════════════════════════════════════
1373
1421
  function UIManager:setStatus(stateStr)
1374
1422
  if stateStr == "disconnected" then
@@ -1379,11 +1427,20 @@ function UIManager:setStatus(stateStr)
1379
1427
  self.termStroke.Color = Color3.fromRGB(40, 40, 48)
1380
1428
 
1381
1429
  elseif stateStr == "waiting" then
1430
+ -- Server tidak dapat dijangkau (bridge belum jalan)
1431
+ self.connectBtn.Text = " Disconnect"
1432
+ self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
1433
+ self.statusLabel.Text = "● Waiting for Server..."
1434
+ self.statusLabel.TextColor3 = Color3.fromRGB(255, 140, 60)
1435
+ self.termStroke.Color = Color3.fromRGB(100, 70, 30)
1436
+
1437
+ elseif stateStr == "server_connected" then
1438
+ -- Server reachable, tapi IDE Agent (Antigravity) belum konek
1382
1439
  self.connectBtn.Text = " Disconnect"
1383
1440
  self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
1384
1441
  self.statusLabel.Text = "● Waiting for AI..."
1385
- self.statusLabel.TextColor3 = Color3.fromRGB(255, 200, 80)
1386
- self.termStroke.Color = Color3.fromRGB(130, 110, 50)
1442
+ self.statusLabel.TextColor3 = Color3.fromRGB(80, 180, 255)
1443
+ self.termStroke.Color = Color3.fromRGB(40, 80, 130)
1387
1444
 
1388
1445
  elseif stateStr == "connected" then
1389
1446
  self.connectBtn.Text = " Disconnect"