@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.
- package/dist/index.js +53 -9
- package/dist/roblox-bridge.js +272 -83
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +349 -301
- package/studio-plugin/nvstudio_mcp.rbxmx +357 -300
- package/studio-plugin/ui_manager.lua +12 -3
|
@@ -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[
|
|
382
|
-
|
|
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
|
-
|
|
385
|
-
local
|
|
386
|
-
local
|
|
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
|
-
--
|
|
389
|
-
local STUDIO_ID
|
|
397
|
+
-- ID unik per sesi Studio
|
|
398
|
+
local STUDIO_ID = HttpService:GenerateGUID(false)
|
|
390
399
|
|
|
391
|
-
--
|
|
400
|
+
-- ── UI Module ──────────────────────────────────────────────────────────────────
|
|
392
401
|
local uiModule = script.Parent:FindFirstChild("ui_manager")
|
|
393
402
|
local ui = nil
|
|
394
403
|
|
|
395
|
-
--
|
|
396
|
-
local toolbar
|
|
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
|
-
--
|
|
408
|
+
-- ── State ─────────────────────────────────────────────────────────────────────
|
|
400
409
|
local ctx = {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
410
|
+
scriptHistory = {},
|
|
411
|
+
recentLogs = {},
|
|
412
|
+
HttpService = HttpService,
|
|
413
|
+
studioId = STUDIO_ID,
|
|
405
414
|
}
|
|
406
|
-
local MAX_LOGS
|
|
407
|
-
local
|
|
408
|
-
local
|
|
409
|
-
local
|
|
410
|
-
local
|
|
411
|
-
local
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
local
|
|
416
|
-
local
|
|
417
|
-
|
|
418
|
-
--
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
--
|
|
487
|
+
-- ── Log Capture ──────────────────────────────────────────────────────────────
|
|
502
488
|
local function addSystemLog(msg, msgType)
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
--
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
--
|
|
520
|
+
-- ── Proses Task dari Server ───────────────────────────────────────────────────
|
|
563
521
|
local function processTask(taskData)
|
|
564
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
--
|
|
602
|
-
function
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
--
|
|
609
|
-
function
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
--
|
|
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
|
-
|
|
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
|
-
--
|
|
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(
|
|
1386
|
-
self.termStroke.Color = Color3.fromRGB(
|
|
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"
|