@adiholick/nvstudio-mcp 2.1.0 → 2.1.2

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.
@@ -380,9 +380,9 @@ end
380
380
  <token name="RunContext">0</token>
381
381
  <ProtectedString name="Source"><![CDATA[-- ══════════════════════════════════════════════════════════════════════
382
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.
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
386
  -- ══════════════════════════════════════════════════════════════════════
387
387
 
388
388
  local HttpService = game:GetService("HttpService")
@@ -390,11 +390,11 @@ local LogService = game:GetService("LogService")
390
390
 
391
391
  -- URL Endpoints
392
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
393
+ local STREAM_URL = BASE_URL .. "/api/stream" -- SSE long-poll (blocking GET)
394
+ local RESPONSE_URL = BASE_URL .. "/api/response"
395
395
  local PING_URL = BASE_URL .. "/api/ping"
396
396
 
397
- -- ID unik per sesi Studio (jika dua Studio terbuka, keduanya didaftarkan terpisah)
397
+ -- ID unik per sesi Studio
398
398
  local STUDIO_ID = HttpService:GenerateGUID(false)
399
399
 
400
400
  -- ── UI Module ──────────────────────────────────────────────────────────────────
@@ -407,368 +407,358 @@ local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP
407
407
 
408
408
  -- ── State ─────────────────────────────────────────────────────────────────────
409
409
  local ctx = {
410
- scriptHistory = {},
411
- recentLogs = {},
412
- HttpService = HttpService,
413
- studioId = STUDIO_ID,
410
+ scriptHistory = {},
411
+ recentLogs = {},
412
+ HttpService = HttpService,
413
+ studioId = STUDIO_ID,
414
414
  }
415
- local MAX_LOGS = 50
416
- local isConnected = false -- apakah SSE stream aktif
417
- local mcpAgentActive = false -- apakah Antigravity/Cursor sedang konek ke MCP
415
+ local MAX_LOGS = 50
416
+ local isRunning = false -- apakah loop aktif
418
417
  local isManualDisconnect = false
419
- local isClickDebounce = false
420
- local currentStream = nil -- WebStreamClient aktif
421
- local streamConnections = {} -- RBXScriptConnection SSE
418
+ local isClickDebounce = false
419
+ local mcpAgentActive = false -- apakah Antigravity sedang konek ke MCP
420
+ local serverReachable = false -- apakah server Node.js reachable
422
421
 
423
- -- Reconnect config (exponential backoff)
424
- local RECONNECT_DELAY_INITIAL = 0.5
425
- local RECONNECT_DELAY_MAX = 10
422
+ -- Reconnect config
423
+ local RECONNECT_DELAY_INITIAL = 1
424
+ local RECONNECT_DELAY_MAX = 15
426
425
  local reconnectDelay = RECONNECT_DELAY_INITIAL
427
426
 
428
427
  -- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
429
428
  local skills = {}
430
429
  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
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
439
438
  end
440
439
 
441
440
  -- ── Inisialisasi UI ──────────────────────────────────────────────────────────
442
441
  if uiModule then
443
- local UIManager = require(uiModule)
444
- ui = UIManager.init(plugin, STUDIO_ID)
445
-
446
- ui:setAutoConnectState(true)
447
- ui:setStatus("waiting")
448
-
449
- if ui.widget then
450
- toggleBtn:SetActive(ui.widget.Enabled)
451
- ui.widget:GetPropertyChangedSignal("Enabled"):Connect(function()
452
- toggleBtn:SetActive(ui.widget.Enabled)
453
- end)
454
- end
455
-
456
- toggleBtn.Click:Connect(function()
457
- ui:toggleVisibility()
458
- end)
459
-
460
- -- Tombol Connect / Disconnect
461
- ui.connectBtn.MouseButton1Click:Connect(function()
462
- if isClickDebounce then return end
463
- isClickDebounce = true
464
-
465
- if not isConnected then
466
- isManualDisconnect = false
467
- reconnectDelay = RECONNECT_DELAY_INITIAL
468
- ui:setStatus("waiting")
469
- ui:addLog("[System] Menghubungkan ulang ke Server MCP...", Color3.fromRGB(255, 220, 100))
470
- task.spawn(connectSSE)
471
- else
472
- isManualDisconnect = true
473
- disconnectSSE()
474
- ui:setStatus("disconnected")
475
- ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
476
- end
477
-
478
- task.delay(0.3, function() isClickDebounce = false end)
479
- 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)
480
481
  else
481
- toggleBtn.Click:Connect(function()
482
- print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan. GUI gagal dimuat!")
483
- end)
482
+ toggleBtn.Click:Connect(function()
483
+ print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan. GUI gagal dimuat!")
484
+ end)
484
485
  end
485
486
 
486
487
  -- ── Log Capture ──────────────────────────────────────────────────────────────
487
488
  local function addSystemLog(msg, msgType)
488
- local color = Color3.fromRGB(200, 200, 200)
489
- local prefix = "[INFO] "
490
- if msgType == Enum.MessageType.MessageError then
491
- prefix = "[ERROR] "; color = Color3.fromRGB(255, 100, 100)
492
- elseif msgType == Enum.MessageType.MessageWarning then
493
- prefix = "[WARN] "; color = Color3.fromRGB(255, 200, 100)
494
- end
495
- local fullMsg = prefix .. msg
496
- table.insert(ctx.recentLogs, fullMsg)
497
- if #ctx.recentLogs > MAX_LOGS then table.remove(ctx.recentLogs, 1) end
498
- if ui then ui:addLog(fullMsg, color) 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
499
500
  end
500
501
  LogService.MessageOut:Connect(addSystemLog)
501
502
 
502
503
  -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
503
504
  local function getInstanceFromPath(pathString)
504
- local parts = string.split(pathString, ".")
505
- local current = game
506
- if parts[1] == "game" then table.remove(parts, 1) end
507
- pcall(function()
508
- local svc = game:GetService(parts[1])
509
- if svc then current = svc; table.remove(parts, 1) end
510
- end)
511
- for _, part in ipairs(parts) do
512
- local found = current:FindFirstChild(part)
513
- if not found then return nil end
514
- current = found
515
- end
516
- 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
517
518
  end
518
519
 
519
520
  -- ── Proses Task dari Server ───────────────────────────────────────────────────
520
521
  local function processTask(taskData)
521
- if not taskData or not taskData.id or not taskData.command then return nil end
522
+ if not taskData or not taskData.id or not taskData.command then return nil end
522
523
 
523
- if ui then
524
- ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
525
- end
524
+ if ui then
525
+ ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
526
+ end
526
527
 
527
- local targetInstance = getInstanceFromPath(taskData.target)
528
- if not targetInstance then
529
- return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
530
- end
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
531
532
 
532
- local skillFunc = skills[taskData.command]
533
- if not skillFunc then
534
- return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
535
- end
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
536
537
 
537
- local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
538
- result.id = taskData.id
538
+ local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
539
+ result.id = taskData.id
539
540
 
540
- if ui then
541
- local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
542
- ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
543
- end
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
544
545
 
545
- return result
546
+ return result
546
547
  end
547
548
 
548
549
  -- ── Kirim Respons ke Server ───────────────────────────────────────────────────
549
550
  local function sendResponse(taskId, resultData)
550
- task.spawn(function()
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
- )
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)
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
580
-
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))
604
- end
605
- end
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)
629
- end
630
- end)
631
- return
632
- end
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)
633
569
  end
634
570
 
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
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
580
+ -- Tampilkan sebagai connected (karena server sse bridge aktif)
581
+ ui:setStatus("server_connected")
582
+ end
646
583
  end
647
584
 
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)
704
- end
705
- end)
706
- table.insert(streamConnections, onClose)
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
707
644
  end
708
645
 
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
-
717
- if ui then
718
- ui:setStatus("waiting")
719
- ui:addLog("[System] Mode Polling aktif (WebStreamClient tidak didukung).", Color3.fromRGB(200, 200, 100))
720
- end
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)
742
- end
743
- end
744
- end
745
- task.wait(0.1)
746
- else
747
- if ui then ui:setStatus("waiting") end
748
- task.wait(1.5)
749
- end
750
- end
751
-
752
- isPollingActive = false
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
+ -- No need to explicitly add log for IDE Agent wait every time it connects,
709
+ -- bridge connected log is enough.
710
+ end
711
+ end
712
+ elseif serverReachable and not mcpAgentActive then
713
+ -- Tidak ada perubahan tapi pastikan status benar
714
+ if ui then ui:setStatus("server_connected") end
715
+ end
716
+ end
717
+
718
+ -- Langkah 2: Buka SSE stream (blocking hingga server menutup koneksi / ~10 detik)
719
+ -- Server akan mengirim events yang tertunda, heartbeat, lalu tutup.
720
+ if isRunning and not isManualDisconnect then
721
+ local streamOk, streamRaw = pcall(function()
722
+ return HttpService:GetAsync(
723
+ STREAM_URL .. "?studioId=" .. HttpService:UrlEncode(STUDIO_ID),
724
+ false
725
+ )
726
+ end)
727
+
728
+ if streamOk and streamRaw and streamRaw ~= "" then
729
+ -- Parse baris-baris SSE dari response
730
+ for line in (streamRaw .. "\n"):gmatch("([^\n]*)\n") do
731
+ line = line:gsub("\r", "")
732
+ if line ~= "" then
733
+ handleSSELine(line)
734
+ end
735
+ end
736
+ end
737
+ end
738
+
739
+ -- Jeda singkat sebelum cycle berikutnya
740
+ task.wait(0.5)
741
+ end
742
+ end
743
+
744
+ isRunning = false
745
+ serverReachable = false
746
+ mcpAgentActive = false
747
+ if ui then ui:setStatus("disconnected") end
753
748
  end
754
749
 
755
750
  -- ── Plugin Unloading ──────────────────────────────────────────────────────────
756
751
  plugin.Unloading:Connect(function()
757
- isManualDisconnect = true
758
- disconnectSSE()
752
+ isManualDisconnect = true
753
+ isRunning = false
759
754
  end)
760
755
 
761
756
  -- ══════════════════════════════════════════════════════════════════════
762
- -- STARTUP: Langsung buka SSE stream
757
+ -- STARTUP: Langsung mulai connection loop
763
758
  -- ══════════════════════════════════════════════════════════════════════
764
759
  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()
760
+ task.wait(0.5) -- Tunggu UI siap
761
+ startConnectionLoop()
772
762
  end)
773
763
  ]]></ProtectedString>
774
764
  </Properties>
@@ -1125,7 +1115,7 @@ function UIManager.init(pluginInstance, studioId)
1125
1115
 
1126
1116
  -- Version
1127
1117
  local versionLabel = Instance.new("TextLabel")
1128
- versionLabel.Text = "v1.2.0 · Hybrid Agent"
1118
+ versionLabel.Text = "v2.1.2"
1129
1119
  versionLabel.Font = Enum.Font.Gotham
1130
1120
  versionLabel.TextSize = 11
1131
1121
  versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
@@ -1427,7 +1417,7 @@ function UIManager:addLog(text, color)
1427
1417
  end
1428
1418
 
1429
1419
  -- ═══════════════════════════════════════════
1430
- -- 3 State Setter (Disconnected, Waiting, Connected)
1420
+ -- 4 State Setter (Disconnected, Waiting, Server Connected, Connected)
1431
1421
  -- ═══════════════════════════════════════════
1432
1422
  function UIManager:setStatus(stateStr)
1433
1423
  if stateStr == "disconnected" then
@@ -1438,11 +1428,20 @@ function UIManager:setStatus(stateStr)
1438
1428
  self.termStroke.Color = Color3.fromRGB(40, 40, 48)
1439
1429
 
1440
1430
  elseif stateStr == "waiting" then
1431
+ -- Server tidak dapat dijangkau (bridge belum jalan)
1432
+ self.connectBtn.Text = " Disconnect"
1433
+ self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
1434
+ self.statusLabel.Text = "● Waiting for Server..."
1435
+ self.statusLabel.TextColor3 = Color3.fromRGB(255, 140, 60)
1436
+ self.termStroke.Color = Color3.fromRGB(100, 70, 30)
1437
+
1438
+ elseif stateStr == "server_connected" then
1439
+ -- Server reachable, tampilkan Connected meskipun agent (IDE) belum konek via stdio
1441
1440
  self.connectBtn.Text = " Disconnect"
1442
1441
  self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
1443
- self.statusLabel.Text = "● Waiting for AI..."
1444
- self.statusLabel.TextColor3 = Color3.fromRGB(255, 200, 80)
1445
- self.termStroke.Color = Color3.fromRGB(130, 110, 50)
1442
+ self.statusLabel.Text = "● Connected"
1443
+ self.statusLabel.TextColor3 = Color3.fromRGB(100, 230, 120)
1444
+ self.termStroke.Color = Color3.fromRGB(50, 120, 60)
1446
1445
 
1447
1446
  elseif stateStr == "connected" then
1448
1447
  self.connectBtn.Text = " Disconnect"