@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.
- package/dist/roblox-bridge.js +96 -65
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +307 -317
- package/studio-plugin/nvstudio_mcp.rbxmx +321 -322
- package/studio-plugin/ui_manager.lua +14 -5
|
@@ -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,358 @@ 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)
|
|
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
|
-
-- ──
|
|
636
|
-
function
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
-- ──
|
|
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
|
+
-- 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
|
-
|
|
758
|
-
|
|
752
|
+
isManualDisconnect = true
|
|
753
|
+
isRunning = false
|
|
759
754
|
end)
|
|
760
755
|
|
|
761
756
|
-- ══════════════════════════════════════════════════════════════════════
|
|
762
|
-
-- STARTUP: Langsung
|
|
757
|
+
-- STARTUP: Langsung mulai connection loop
|
|
763
758
|
-- ══════════════════════════════════════════════════════════════════════
|
|
764
759
|
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()
|
|
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 = "
|
|
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
|
-
--
|
|
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 = "●
|
|
1444
|
-
self.statusLabel.TextColor3 = Color3.fromRGB(
|
|
1445
|
-
self.termStroke.Color = Color3.fromRGB(
|
|
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"
|