@adiholick/nvstudio-mcp 2.1.0 → 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.
- package/dist/roblox-bridge.js +96 -65
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +306 -317
- package/studio-plugin/nvstudio_mcp.rbxmx +318 -320
- package/studio-plugin/ui_manager.lua +12 -3
|
@@ -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
|
|
384
|
-
-- Plugin
|
|
385
|
-
--
|
|
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"
|
|
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
|
|
397
|
+
-- ID unik per sesi Studio
|
|
398
398
|
local STUDIO_ID = HttpService:GenerateGUID(false)
|
|
399
399
|
|
|
400
400
|
-- ── UI Module ──────────────────────────────────────────────────────────────────
|
|
@@ -407,368 +407,357 @@ local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP
|
|
|
407
407
|
|
|
408
408
|
-- ── State ─────────────────────────────────────────────────────────────────────
|
|
409
409
|
local ctx = {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
scriptHistory = {},
|
|
411
|
+
recentLogs = {},
|
|
412
|
+
HttpService = HttpService,
|
|
413
|
+
studioId = STUDIO_ID,
|
|
414
414
|
}
|
|
415
|
-
local MAX_LOGS
|
|
416
|
-
local
|
|
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
|
|
420
|
-
local
|
|
421
|
-
local
|
|
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
|
|
424
|
-
local RECONNECT_DELAY_INITIAL =
|
|
425
|
-
local RECONNECT_DELAY_MAX =
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
522
|
+
if not taskData or not taskData.id or not taskData.command then return nil end
|
|
522
523
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
524
|
+
if ui then
|
|
525
|
+
ui:addLog("[AI Command] " .. taskData.command, Color3.fromRGB(150, 200, 255))
|
|
526
|
+
end
|
|
526
527
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
538
|
-
|
|
538
|
+
local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
|
|
539
|
+
result.id = taskData.id
|
|
539
540
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
546
|
+
return result
|
|
546
547
|
end
|
|
547
548
|
|
|
548
549
|
-- ── Kirim Respons ke Server ───────────────────────────────────────────────────
|
|
549
550
|
local function sendResponse(taskId, resultData)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
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)
|
|
569
569
|
end
|
|
570
570
|
|
|
571
|
-
-- ──
|
|
572
|
-
local function
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
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
|
|
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
|
|
646
583
|
end
|
|
647
584
|
|
|
648
|
-
-- ──
|
|
649
|
-
function
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
-- ──
|
|
710
|
-
--
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
753
747
|
end
|
|
754
748
|
|
|
755
749
|
-- ── Plugin Unloading ──────────────────────────────────────────────────────────
|
|
756
750
|
plugin.Unloading:Connect(function()
|
|
757
|
-
|
|
758
|
-
|
|
751
|
+
isManualDisconnect = true
|
|
752
|
+
isRunning = false
|
|
759
753
|
end)
|
|
760
754
|
|
|
761
755
|
-- ══════════════════════════════════════════════════════════════════════
|
|
762
|
-
-- STARTUP: Langsung
|
|
756
|
+
-- STARTUP: Langsung mulai connection loop
|
|
763
757
|
-- ══════════════════════════════════════════════════════════════════════
|
|
764
758
|
task.spawn(function()
|
|
765
|
-
|
|
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()
|
|
759
|
+
task.wait(0.5) -- Tunggu UI siap
|
|
760
|
+
startConnectionLoop()
|
|
772
761
|
end)
|
|
773
762
|
]]></ProtectedString>
|
|
774
763
|
</Properties>
|
|
@@ -1427,7 +1416,7 @@ function UIManager:addLog(text, color)
|
|
|
1427
1416
|
end
|
|
1428
1417
|
|
|
1429
1418
|
-- ═══════════════════════════════════════════
|
|
1430
|
-
--
|
|
1419
|
+
-- 4 State Setter (Disconnected, Waiting, Server Connected, Connected)
|
|
1431
1420
|
-- ═══════════════════════════════════════════
|
|
1432
1421
|
function UIManager:setStatus(stateStr)
|
|
1433
1422
|
if stateStr == "disconnected" then
|
|
@@ -1438,11 +1427,20 @@ function UIManager:setStatus(stateStr)
|
|
|
1438
1427
|
self.termStroke.Color = Color3.fromRGB(40, 40, 48)
|
|
1439
1428
|
|
|
1440
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
|
|
1441
1439
|
self.connectBtn.Text = " Disconnect"
|
|
1442
1440
|
self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
|
|
1443
1441
|
self.statusLabel.Text = "● Waiting for AI..."
|
|
1444
|
-
self.statusLabel.TextColor3 = Color3.fromRGB(
|
|
1445
|
-
self.termStroke.Color = Color3.fromRGB(
|
|
1442
|
+
self.statusLabel.TextColor3 = Color3.fromRGB(80, 180, 255)
|
|
1443
|
+
self.termStroke.Color = Color3.fromRGB(40, 80, 130)
|
|
1446
1444
|
|
|
1447
1445
|
elseif stateStr == "connected" then
|
|
1448
1446
|
self.connectBtn.Text = " Disconnect"
|