@adiholick/nvstudio-mcp 1.0.4 → 2.0.0

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.
@@ -0,0 +1,97 @@
1
+ local HttpService = game:GetService("HttpService")
2
+
3
+ return function(targetInstance, data, ctx, targetPath)
4
+ if not targetInstance:IsA("LuaSourceContainer") then
5
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
6
+ end
7
+
8
+ local payload = nil
9
+ pcall(function()
10
+ payload = HttpService:JSONDecode(data)
11
+ end)
12
+
13
+ if not payload then
14
+ return { status = "error", error = "Payload data harus berupa JSON valid." }
15
+ end
16
+
17
+ local currentSource = targetInstance.Source
18
+ local newSource = currentSource
19
+ local affectedLines = 0
20
+
21
+ -- Mode 1: Search & Replace Exact Chunk
22
+ if payload.target_content and payload.replacement_content then
23
+ local targetContent = payload.target_content
24
+ local replacementContent = payload.replacement_content
25
+ local allowMultiple = payload.allow_multiple or false
26
+
27
+ -- Escape magic characters for string.gsub if needed, or use string.find manually.
28
+ -- string.gsub doesn't do plain text replacement directly without escaping.
29
+ -- We'll write a plain text replace function.
30
+
31
+ local function plainReplace(str, pattern, repl, max)
32
+ local res = ""
33
+ local startIdx = 1
34
+ local count = 0
35
+ while true do
36
+ if max and count >= max then break end
37
+ local findStart, findEnd = string.find(str, pattern, startIdx, true) -- true for plain match
38
+ if not findStart then break end
39
+
40
+ res = res .. string.sub(str, startIdx, findStart - 1) .. repl
41
+ startIdx = findEnd + 1
42
+ count = count + 1
43
+ end
44
+ res = res .. string.sub(str, startIdx)
45
+ return res, count
46
+ end
47
+
48
+ local _, occurrences = plainReplace(currentSource, targetContent, replacementContent)
49
+
50
+ if occurrences == 0 then
51
+ return { status = "error", error = "target_content tidak ditemukan di dalam script." }
52
+ elseif occurrences > 1 and not allowMultiple then
53
+ return { status = "error", error = "target_content ditemukan " .. occurrences .. " kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
54
+ end
55
+
56
+ newSource, affectedLines = plainReplace(currentSource, targetContent, replacementContent)
57
+ affectedLines = affectedLines -- not exact line count, but occurrences count
58
+
59
+ -- Mode 2: Line Range Replace
60
+ elseif payload.start_line and payload.end_line and payload.replacement_content then
61
+ local lines = string.split(currentSource, "\n")
62
+ local startLine = tonumber(payload.start_line)
63
+ local endLine = tonumber(payload.end_line)
64
+
65
+ if not startLine or not endLine or startLine < 1 or endLine > #lines or startLine > endLine then
66
+ return { status = "error", error = "Range baris tidak valid. Jumlah baris script: " .. #lines }
67
+ end
68
+
69
+ local newLines = {}
70
+ for i = 1, startLine - 1 do
71
+ table.insert(newLines, lines[i])
72
+ end
73
+
74
+ table.insert(newLines, payload.replacement_content)
75
+
76
+ for i = endLine + 1, #lines do
77
+ table.insert(newLines, lines[i])
78
+ end
79
+
80
+ newSource = table.concat(newLines, "\n")
81
+ affectedLines = (endLine - startLine) + 1
82
+ else
83
+ return { status = "error", error = "Payload tidak lengkap. Butuh (target_content & replacement_content) atau (start_line, end_line & replacement_content)." }
84
+ end
85
+
86
+ -- Simpan backup snapshot sebelum mengaplikasikan patch
87
+ ctx.scriptHistory[targetPath] = currentSource
88
+
89
+ -- Terapkan perubahan
90
+ targetInstance.Source = newSource
91
+
92
+ return {
93
+ status = "success",
94
+ result = "Patch berhasil diterapkan pada '" .. targetInstance.Name .. "'. Perubahan diaplikasikan.",
95
+ affected_lines = affectedLines
96
+ }
97
+ end
@@ -0,0 +1,13 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ if targetInstance:IsA("LuaSourceContainer") then
3
+ local prevSource = ctx.scriptHistory[targetPath]
4
+ if prevSource then
5
+ targetInstance.Source = prevSource
6
+ return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya." }
7
+ else
8
+ return { status = "error", error = "Tidak ada riwayat backup sebelumnya di sesi Studio ini untuk: " .. targetPath }
9
+ end
10
+ else
11
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local query = tostring(data)
3
+ if not query or query == "" then
4
+ return { status = "error", error = "Query pencarian tidak boleh kosong." }
5
+ end
6
+
7
+ local matches = {}
8
+ local matchCount = 0
9
+ local MAX_MATCHES = 50
10
+
11
+ local searchableServices = {
12
+ game:GetService("Workspace"),
13
+ game:GetService("ServerScriptService"),
14
+ game:GetService("ReplicatedStorage"),
15
+ game:GetService("StarterPlayer"),
16
+ game:GetService("StarterGui")
17
+ }
18
+
19
+ local function searchInScript(scriptObj)
20
+ local success, source = pcall(function() return scriptObj.Source end)
21
+ if not success or not source then return end
22
+
23
+ local lines = string.split(source, "\n")
24
+ for i, line in ipairs(lines) do
25
+ -- Coba cari dengan plain text dulu, jika gagal coba pattern
26
+ local found = string.find(line, query, 1, true)
27
+ if not found then
28
+ pcall(function() found = string.find(line, query) end)
29
+ end
30
+
31
+ if found then
32
+ table.insert(matches, {
33
+ script = scriptObj:GetFullName(),
34
+ line = i,
35
+ content = string.match(line, "^%s*(.-)%s*$") or line
36
+ })
37
+ matchCount = matchCount + 1
38
+ if matchCount >= MAX_MATCHES then
39
+ return true
40
+ end
41
+ end
42
+ end
43
+ return false
44
+ end
45
+
46
+ for _, service in ipairs(searchableServices) do
47
+ pcall(function()
48
+ local descendants = service:GetDescendants()
49
+ for _, desc in ipairs(descendants) do
50
+ if desc:IsA("LuaSourceContainer") then
51
+ if searchInScript(desc) then
52
+ break
53
+ end
54
+ end
55
+ end
56
+ end)
57
+ if matchCount >= MAX_MATCHES then
58
+ break
59
+ end
60
+ end
61
+
62
+ return { status = "success", matches = matches, count = matchCount, limitReached = (matchCount >= MAX_MATCHES) }
63
+ end
@@ -0,0 +1,20 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local searchName = tostring(data)
3
+ local results = {}
4
+
5
+ local searchableServices = {game:GetService("Workspace"), game:GetService("ReplicatedStorage"), game:GetService("ServerScriptService"), game:GetService("StarterGui")}
6
+
7
+ for _, service in ipairs(searchableServices) do
8
+ for _, desc in ipairs(service:GetDescendants()) do
9
+ if desc.Name == searchName then
10
+ table.insert(results, desc:GetFullName())
11
+ end
12
+ end
13
+ end
14
+
15
+ if #results > 0 then
16
+ return { status = "success", result = results }
17
+ else
18
+ return { status = "success", result = "Tidak ditemukan instance dengan nama: " .. searchName }
19
+ end
20
+ end
@@ -0,0 +1,443 @@
1
+ local UIManager = {}
2
+ local widget = nil
3
+
4
+ function UIManager.init(pluginInstance, studioId)
5
+ -- Membuat Widget Panel yang bisa di-dock (menempel)
6
+ local widgetInfo = DockWidgetPluginGuiInfo.new(
7
+ Enum.InitialDockState.Right,
8
+ false, -- Initially hidden until toolbar button is clicked
9
+ false, -- Override enabled
10
+ 380, -- Default width
11
+ 520, -- Default height
12
+ 280, -- Min width
13
+ 320 -- Min height
14
+ )
15
+
16
+ widget = pluginInstance:CreateDockWidgetPluginGui("NVStudioMCP_GUI_V3", widgetInfo)
17
+ widget.Title = "NVStudio MCP Dashboard"
18
+
19
+ -- ═══════════════════════════════════════════
20
+ -- ROOT FRAME (Non-scrolling, fills entire widget)
21
+ -- ═══════════════════════════════════════════
22
+ local rootFrame = Instance.new("Frame")
23
+ rootFrame.Size = UDim2.new(1, 0, 1, 0)
24
+ rootFrame.BackgroundColor3 = Color3.fromRGB(22, 22, 26)
25
+ rootFrame.BorderSizePixel = 0
26
+ rootFrame.Parent = widget
27
+
28
+ local rootPadding = Instance.new("UIPadding")
29
+ rootPadding.PaddingTop = UDim.new(0, 8)
30
+ rootPadding.PaddingLeft = UDim.new(0, 10)
31
+ rootPadding.PaddingRight = UDim.new(0, 10)
32
+ rootPadding.PaddingBottom = UDim.new(0, 8)
33
+ rootPadding.Parent = rootFrame
34
+
35
+ local rootLayout = Instance.new("UIListLayout")
36
+ rootLayout.SortOrder = Enum.SortOrder.LayoutOrder
37
+ rootLayout.Padding = UDim.new(0, 6)
38
+ rootLayout.Parent = rootFrame
39
+
40
+ -- ═══════════════════════════════════════════
41
+ -- SECTION 1: HEADER CARD
42
+ -- ═══════════════════════════════════════════
43
+ local headerFrame = Instance.new("Frame")
44
+ headerFrame.AutomaticSize = Enum.AutomaticSize.Y
45
+ headerFrame.Size = UDim2.new(1, 0, 0, 0)
46
+ headerFrame.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
47
+ headerFrame.BorderSizePixel = 0
48
+ headerFrame.LayoutOrder = 1
49
+ headerFrame.Parent = rootFrame
50
+
51
+ local hCorner = Instance.new("UICorner")
52
+ hCorner.CornerRadius = UDim.new(0, 6)
53
+ hCorner.Parent = headerFrame
54
+
55
+ local hPadding = Instance.new("UIPadding")
56
+ hPadding.PaddingTop = UDim.new(0, 10)
57
+ hPadding.PaddingLeft = UDim.new(0, 12)
58
+ hPadding.PaddingRight = UDim.new(0, 12)
59
+ hPadding.PaddingBottom = UDim.new(0, 10)
60
+ hPadding.Parent = headerFrame
61
+
62
+ local hLayout = Instance.new("UIListLayout")
63
+ hLayout.SortOrder = Enum.SortOrder.LayoutOrder
64
+ hLayout.Padding = UDim.new(0, 2)
65
+ hLayout.Parent = headerFrame
66
+
67
+ -- Title Row (Name + Status side by side)
68
+ local titleRow = Instance.new("Frame")
69
+ titleRow.AutomaticSize = Enum.AutomaticSize.Y
70
+ titleRow.Size = UDim2.new(1, 0, 0, 0)
71
+ titleRow.BackgroundTransparency = 1
72
+ titleRow.LayoutOrder = 1
73
+ titleRow.Parent = headerFrame
74
+
75
+ local titleLabel = Instance.new("TextLabel")
76
+ titleLabel.Text = "NVStudio MCP"
77
+ titleLabel.Font = Enum.Font.GothamBold
78
+ titleLabel.TextSize = 18
79
+ titleLabel.TextColor3 = Color3.fromRGB(240, 240, 245)
80
+ titleLabel.BackgroundTransparency = 1
81
+ titleLabel.Size = UDim2.new(0.6, 0, 0, 22)
82
+ titleLabel.TextXAlignment = Enum.TextXAlignment.Left
83
+ titleLabel.Parent = titleRow
84
+
85
+ local statusLabel = Instance.new("TextLabel")
86
+ statusLabel.Text = "● Disconnected"
87
+ statusLabel.Font = Enum.Font.GothamBold
88
+ statusLabel.TextSize = 12
89
+ statusLabel.TextColor3 = Color3.fromRGB(255, 100, 100)
90
+ statusLabel.BackgroundTransparency = 1
91
+ statusLabel.Size = UDim2.new(0.4, 0, 0, 22)
92
+ statusLabel.Position = UDim2.new(0.6, 0, 0, 0)
93
+ statusLabel.TextXAlignment = Enum.TextXAlignment.Right
94
+ statusLabel.Parent = titleRow
95
+
96
+ -- Version
97
+ local versionLabel = Instance.new("TextLabel")
98
+ versionLabel.Text = "v1.2.0 · Hybrid Agent"
99
+ versionLabel.Font = Enum.Font.Gotham
100
+ versionLabel.TextSize = 11
101
+ versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
102
+ versionLabel.BackgroundTransparency = 1
103
+ versionLabel.Size = UDim2.new(1, 0, 0, 14)
104
+ versionLabel.TextXAlignment = Enum.TextXAlignment.Left
105
+ versionLabel.LayoutOrder = 2
106
+ versionLabel.Parent = headerFrame
107
+
108
+ -- Thin separator
109
+ local sep1 = Instance.new("Frame")
110
+ sep1.Size = UDim2.new(1, 0, 0, 1)
111
+ sep1.BackgroundColor3 = Color3.fromRGB(50, 50, 58)
112
+ sep1.BorderSizePixel = 0
113
+ sep1.LayoutOrder = 3
114
+ sep1.Parent = headerFrame
115
+
116
+ -- Place info
117
+ local placeLabel = Instance.new("TextLabel")
118
+ placeLabel.Text = "📁 " .. (game.Name ~= "" and game.Name or "Untitled Baseplate")
119
+ placeLabel.Font = Enum.Font.Gotham
120
+ placeLabel.TextSize = 11
121
+ placeLabel.TextColor3 = Color3.fromRGB(180, 180, 190)
122
+ placeLabel.BackgroundTransparency = 1
123
+ placeLabel.Size = UDim2.new(1, 0, 0, 16)
124
+ placeLabel.TextXAlignment = Enum.TextXAlignment.Left
125
+ placeLabel.LayoutOrder = 4
126
+ placeLabel.Parent = headerFrame
127
+
128
+ -- Studio ID
129
+ local idLabel = Instance.new("TextLabel")
130
+ idLabel.Text = "🔗 " .. string.sub(tostring(studioId), 1, 18) .. "..."
131
+ idLabel.Font = Enum.Font.Code
132
+ idLabel.TextSize = 10
133
+ idLabel.TextColor3 = Color3.fromRGB(90, 180, 230)
134
+ idLabel.BackgroundTransparency = 1
135
+ idLabel.Size = UDim2.new(1, 0, 0, 14)
136
+ idLabel.TextXAlignment = Enum.TextXAlignment.Left
137
+ idLabel.LayoutOrder = 5
138
+ idLabel.Parent = headerFrame
139
+
140
+ -- Server URL
141
+ local serverLabel = Instance.new("TextLabel")
142
+ serverLabel.Text = "🌐 localhost:3055"
143
+ serverLabel.Font = Enum.Font.Code
144
+ serverLabel.TextSize = 10
145
+ serverLabel.TextColor3 = Color3.fromRGB(140, 140, 150)
146
+ serverLabel.BackgroundTransparency = 1
147
+ serverLabel.Size = UDim2.new(1, 0, 0, 14)
148
+ serverLabel.TextXAlignment = Enum.TextXAlignment.Left
149
+ serverLabel.LayoutOrder = 6
150
+ serverLabel.Parent = headerFrame
151
+
152
+ -- ═══════════════════════════════════════════
153
+ -- SECTION 2: CONTROLS ROW
154
+ -- ═══════════════════════════════════════════
155
+ local controlsFrame = Instance.new("Frame")
156
+ controlsFrame.Size = UDim2.new(1, 0, 0, 32)
157
+ controlsFrame.BackgroundTransparency = 1
158
+ controlsFrame.LayoutOrder = 2
159
+ controlsFrame.Parent = rootFrame
160
+
161
+ -- Connect / Disconnect Button
162
+ local connectBtn = Instance.new("TextButton")
163
+ connectBtn.Size = UDim2.new(0.52, -4, 1, 0)
164
+ connectBtn.Position = UDim2.new(0, 0, 0, 0)
165
+ connectBtn.Text = " Connect"
166
+ connectBtn.Font = Enum.Font.GothamBold
167
+ connectBtn.TextSize = 13
168
+ connectBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
169
+ connectBtn.BackgroundColor3 = Color3.fromRGB(40, 160, 80)
170
+ connectBtn.BorderSizePixel = 0
171
+ connectBtn.AutoButtonColor = false
172
+ connectBtn.ZIndex = 2
173
+ connectBtn.Parent = controlsFrame
174
+
175
+ local cCorner = Instance.new("UICorner")
176
+ cCorner.CornerRadius = UDim.new(0, 5)
177
+ cCorner.Parent = connectBtn
178
+
179
+ -- Hover Effects for Connect Button (menggunakan stored base color, bukan text check)
180
+ local _btnBaseColor = Color3.fromRGB(40, 160, 80)
181
+ local _isHovering = false
182
+
183
+ connectBtn.MouseEnter:Connect(function()
184
+ _isHovering = true
185
+ connectBtn.BackgroundColor3 = Color3.fromRGB(
186
+ math.min(255, math.floor(_btnBaseColor.R * 255 + 25)),
187
+ math.min(255, math.floor(_btnBaseColor.G * 255 + 25)),
188
+ math.min(255, math.floor(_btnBaseColor.B * 255 + 25))
189
+ )
190
+ end)
191
+ connectBtn.MouseLeave:Connect(function()
192
+ _isHovering = false
193
+ connectBtn.BackgroundColor3 = _btnBaseColor
194
+ end)
195
+
196
+ -- Auto-Connect Checkbox Container
197
+ local autoConnectContainer = Instance.new("Frame")
198
+ autoConnectContainer.Size = UDim2.new(0.48, -4, 1, 0)
199
+ autoConnectContainer.Position = UDim2.new(0.52, 4, 0, 0)
200
+ autoConnectContainer.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
201
+ autoConnectContainer.BorderSizePixel = 0
202
+ autoConnectContainer.Parent = controlsFrame
203
+
204
+ local acCorner = Instance.new("UICorner")
205
+ acCorner.CornerRadius = UDim.new(0, 5)
206
+ acCorner.Parent = autoConnectContainer
207
+
208
+ -- Checkbox visual (outer box)
209
+ local checkboxOuter = Instance.new("Frame")
210
+ checkboxOuter.Size = UDim2.new(0, 16, 0, 16)
211
+ checkboxOuter.Position = UDim2.new(0, 10, 0.5, -8)
212
+ checkboxOuter.BackgroundColor3 = Color3.fromRGB(55, 55, 65)
213
+ checkboxOuter.BorderSizePixel = 0
214
+ checkboxOuter.Parent = autoConnectContainer
215
+
216
+ local cbCorner = Instance.new("UICorner")
217
+ cbCorner.CornerRadius = UDim.new(0, 3)
218
+ cbCorner.Parent = checkboxOuter
219
+
220
+ local cbStroke = Instance.new("UIStroke")
221
+ cbStroke.Color = Color3.fromRGB(80, 80, 95)
222
+ cbStroke.Thickness = 1
223
+ cbStroke.Parent = checkboxOuter
224
+
225
+ -- Checkbox inner (check mark)
226
+ local checkboxInner = Instance.new("Frame")
227
+ checkboxInner.Size = UDim2.new(0, 10, 0, 10)
228
+ checkboxInner.Position = UDim2.new(0.5, -5, 0.5, -5)
229
+ checkboxInner.BackgroundColor3 = Color3.fromRGB(80, 170, 255)
230
+ checkboxInner.Visible = false
231
+ checkboxInner.Parent = checkboxOuter
232
+
233
+ local cbiCorner = Instance.new("UICorner")
234
+ cbiCorner.CornerRadius = UDim.new(0, 2)
235
+ cbiCorner.Parent = checkboxInner
236
+
237
+ -- Label
238
+ local autoConnectLabel = Instance.new("TextLabel")
239
+ autoConnectLabel.Text = "Auto-Connect"
240
+ autoConnectLabel.Font = Enum.Font.Gotham
241
+ autoConnectLabel.TextSize = 11
242
+ autoConnectLabel.TextColor3 = Color3.fromRGB(180, 180, 190)
243
+ autoConnectLabel.BackgroundTransparency = 1
244
+ autoConnectLabel.Position = UDim2.new(0, 32, 0, 0)
245
+ autoConnectLabel.Size = UDim2.new(1, -36, 1, 0)
246
+ autoConnectLabel.TextXAlignment = Enum.TextXAlignment.Left
247
+ autoConnectLabel.Parent = autoConnectContainer
248
+
249
+ -- Invisible overlay button for checkbox hit area
250
+ local autoConnectBtn = Instance.new("TextButton")
251
+ autoConnectBtn.Size = UDim2.new(1, 0, 1, 0)
252
+ autoConnectBtn.BackgroundTransparency = 1
253
+ autoConnectBtn.Text = ""
254
+ autoConnectBtn.ZIndex = 3 -- Di atas elemen visual agar klik selalu terdeteksi
255
+ autoConnectBtn.Parent = autoConnectContainer
256
+
257
+ -- Hover effect for checkbox container
258
+ autoConnectBtn.MouseEnter:Connect(function()
259
+ autoConnectContainer.BackgroundColor3 = Color3.fromRGB(42, 42, 50)
260
+ end)
261
+ autoConnectBtn.MouseLeave:Connect(function()
262
+ autoConnectContainer.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
263
+ end)
264
+
265
+ -- ═══════════════════════════════════════════
266
+ -- SECTION 3: TERMINAL LOG (fills remaining space)
267
+ -- ═══════════════════════════════════════════
268
+ -- Terminal Label
269
+ local termLabel = Instance.new("TextLabel")
270
+ termLabel.Text = "TERMINAL"
271
+ termLabel.Font = Enum.Font.GothamBold
272
+ termLabel.TextSize = 10
273
+ termLabel.TextColor3 = Color3.fromRGB(90, 90, 100)
274
+ termLabel.BackgroundTransparency = 1
275
+ termLabel.Size = UDim2.new(1, 0, 0, 14)
276
+ termLabel.TextXAlignment = Enum.TextXAlignment.Left
277
+ termLabel.LayoutOrder = 3
278
+ termLabel.Parent = rootFrame
279
+
280
+ -- Terminal container: Fills ALL remaining vertical space
281
+ local terminalContainer = Instance.new("Frame")
282
+ terminalContainer.BackgroundColor3 = Color3.fromRGB(14, 14, 18)
283
+ terminalContainer.BorderSizePixel = 0
284
+ terminalContainer.LayoutOrder = 4
285
+ terminalContainer.ClipsDescendants = true
286
+ terminalContainer.Parent = rootFrame
287
+
288
+ local termCorner = Instance.new("UICorner")
289
+ termCorner.CornerRadius = UDim.new(0, 5)
290
+ termCorner.Parent = terminalContainer
291
+
292
+ local termStroke = Instance.new("UIStroke")
293
+ termStroke.Color = Color3.fromRGB(40, 40, 48)
294
+ termStroke.Thickness = 1
295
+ termStroke.Parent = terminalContainer
296
+
297
+ -- Scrolling log area inside terminal
298
+ local scrollingFrame = Instance.new("ScrollingFrame")
299
+ scrollingFrame.Size = UDim2.new(1, -8, 1, -4)
300
+ scrollingFrame.Position = UDim2.new(0, 4, 0, 2)
301
+ scrollingFrame.BackgroundTransparency = 1
302
+ scrollingFrame.BorderSizePixel = 0
303
+ scrollingFrame.ScrollBarThickness = 4
304
+ scrollingFrame.ScrollBarImageColor3 = Color3.fromRGB(70, 70, 85)
305
+ scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
306
+ scrollingFrame.Parent = terminalContainer
307
+
308
+ local listLayout = Instance.new("UIListLayout")
309
+ listLayout.Padding = UDim.new(0, 1)
310
+ listLayout.SortOrder = Enum.SortOrder.LayoutOrder
311
+ listLayout.Parent = scrollingFrame
312
+
313
+ -- ═══════════════════════════════════════════
314
+ -- RESPONSIVITY: Dynamically size terminal to fill remaining space
315
+ -- ═══════════════════════════════════════════
316
+ local function updateLayout()
317
+ -- Calculate how much space header + controls + termLabel use
318
+ local usedHeight = rootLayout.AbsoluteContentSize.Y - terminalContainer.AbsoluteSize.Y
319
+ local availableHeight = rootFrame.AbsoluteSize.Y - 16 -- rootPadding top+bottom
320
+ local termHeight = math.max(100, availableHeight - usedHeight)
321
+ terminalContainer.Size = UDim2.new(1, 0, 0, termHeight)
322
+ end
323
+
324
+ -- Fire on widget resize
325
+ widget:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateLayout)
326
+ -- Also fire once after initial render
327
+ task.defer(updateLayout)
328
+
329
+ -- ═══════════════════════════════════════════
330
+ -- Store references for external access
331
+ -- ═══════════════════════════════════════════
332
+ UIManager.widget = widget
333
+ UIManager.connectBtn = connectBtn
334
+ UIManager.autoConnectBtn = autoConnectBtn
335
+ UIManager.checkboxInner = checkboxInner
336
+ UIManager.checkboxStroke = cbStroke
337
+ UIManager.statusLabel = statusLabel
338
+ UIManager.scrollingFrame = scrollingFrame
339
+ UIManager.listLayout = listLayout
340
+ UIManager.terminalContainer = terminalContainer
341
+ UIManager.termStroke = termStroke
342
+ UIManager.logsCount = 0
343
+
344
+ -- Simpan referensi hover state agar setStatus bisa update base color
345
+ UIManager._btnBaseColor = _btnBaseColor
346
+ UIManager._isHovering = _isHovering
347
+ UIManager._updateBtnBaseColor = function(newColor)
348
+ _btnBaseColor = newColor
349
+ if not _isHovering then
350
+ connectBtn.BackgroundColor3 = newColor
351
+ end
352
+ end
353
+
354
+ return UIManager
355
+ end
356
+
357
+ -- ═══════════════════════════════════════════
358
+ -- Menambahkan baris log baru ke terminal GUI
359
+ -- ═══════════════════════════════════════════
360
+ function UIManager:addLog(text, color)
361
+ local timestamp = os.date("%H:%M:%S")
362
+ local displayText = "[" .. timestamp .. "] " .. text
363
+
364
+ local logLabel = Instance.new("TextLabel")
365
+ logLabel.Text = displayText
366
+ logLabel.Font = Enum.Font.Code
367
+ logLabel.TextSize = 11
368
+ logLabel.TextColor3 = color or Color3.fromRGB(180, 180, 190)
369
+ logLabel.BackgroundTransparency = 1
370
+ logLabel.TextWrapped = true
371
+ logLabel.TextXAlignment = Enum.TextXAlignment.Left
372
+ logLabel.RichText = false
373
+ logLabel.AutomaticSize = Enum.AutomaticSize.Y
374
+ logLabel.Size = UDim2.new(1, -6, 0, 0) -- Width fill, height auto
375
+
376
+ local logPadding = Instance.new("UIPadding")
377
+ logPadding.PaddingLeft = UDim.new(0, 4)
378
+ logPadding.PaddingTop = UDim.new(0, 1)
379
+ logPadding.PaddingBottom = UDim.new(0, 1)
380
+ logPadding.Parent = logLabel
381
+
382
+ logLabel.LayoutOrder = self.logsCount
383
+ logLabel.Parent = self.scrollingFrame
384
+ self.logsCount = self.logsCount + 1
385
+
386
+ -- Limit max logs to prevent memory bloat
387
+ if self.logsCount > 150 then
388
+ local first = self.scrollingFrame:FindFirstChildWhichIsA("TextLabel")
389
+ if first then first:Destroy() end
390
+ end
391
+
392
+ -- Update canvas size after a frame so AutomaticSize has resolved
393
+ task.defer(function()
394
+ self.scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, self.listLayout.AbsoluteContentSize.Y + 4)
395
+ self.scrollingFrame.CanvasPosition = Vector2.new(0, math.max(0, self.listLayout.AbsoluteContentSize.Y - self.scrollingFrame.AbsoluteSize.Y))
396
+ end)
397
+ end
398
+
399
+ -- ═══════════════════════════════════════════
400
+ -- 3 State Setter (Disconnected, Waiting, Connected)
401
+ -- ═══════════════════════════════════════════
402
+ function UIManager:setStatus(stateStr)
403
+ if stateStr == "disconnected" then
404
+ self.connectBtn.Text = " Connect"
405
+ self._updateBtnBaseColor(Color3.fromRGB(40, 160, 80))
406
+ self.statusLabel.Text = "● Disconnected"
407
+ self.statusLabel.TextColor3 = Color3.fromRGB(255, 100, 100)
408
+ self.termStroke.Color = Color3.fromRGB(40, 40, 48)
409
+
410
+ elseif stateStr == "waiting" then
411
+ self.connectBtn.Text = " Disconnect"
412
+ self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
413
+ self.statusLabel.Text = "● Waiting for AI..."
414
+ self.statusLabel.TextColor3 = Color3.fromRGB(255, 200, 80)
415
+ self.termStroke.Color = Color3.fromRGB(130, 110, 50)
416
+
417
+ elseif stateStr == "connected" then
418
+ self.connectBtn.Text = " Disconnect"
419
+ self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
420
+ self.statusLabel.Text = "● Connected"
421
+ self.statusLabel.TextColor3 = Color3.fromRGB(100, 230, 120)
422
+ self.termStroke.Color = Color3.fromRGB(50, 120, 60)
423
+ end
424
+ end
425
+
426
+ -- Mengubah visual custom checkbox
427
+ function UIManager:setAutoConnectState(isAuto)
428
+ self.checkboxInner.Visible = isAuto
429
+ if isAuto then
430
+ self.checkboxStroke.Color = Color3.fromRGB(80, 170, 255)
431
+ else
432
+ self.checkboxStroke.Color = Color3.fromRGB(80, 80, 95)
433
+ end
434
+ end
435
+
436
+ -- Sakelar visibilitas panel GUI
437
+ function UIManager:toggleVisibility()
438
+ if self.widget then
439
+ self.widget.Enabled = not self.widget.Enabled
440
+ end
441
+ end
442
+
443
+ return UIManager
@@ -0,0 +1,16 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ if targetInstance:IsA("LuaSourceContainer") then
3
+ local newCode = data or ""
4
+
5
+ -- Catatan: Validasi AST menggunakan loadstring() sengaja DIHAPUS.
6
+ -- Karena secara default Roblox Studio mematikan LoadStringEnabled, menggunakannya
7
+ -- hanya akan membuat AI gagal terus menerus untuk menulis script.
8
+ -- Error syntax akan ditangkap secara native oleh Roblox Output.
9
+
10
+ ctx.scriptHistory[targetPath] = targetInstance.Source
11
+ targetInstance.Source = newCode
12
+ return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil diperbarui." }
13
+ else
14
+ return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
15
+ end
16
+ end