@chrrxs/robloxstudio-mcp 2.9.1 → 2.10.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.
@@ -10,6 +10,11 @@ local State = TS.import(script, script, "modules", "State")
10
10
  local UI = TS.import(script, script, "modules", "UI")
11
11
  local Communication = TS.import(script, script, "modules", "Communication")
12
12
  local ClientBroker = TS.import(script, script, "modules", "ClientBroker")
13
+ local RuntimeLogBuffer = TS.import(script, script, "modules", "RuntimeLogBuffer")
14
+ -- Attach the per-peer LogService.MessageOut listener as early as possible so
15
+ -- boot-time prints from the user's place scripts are captured. Powers the
16
+ -- get_runtime_logs MCP tool. Idempotent; safe to call before UI.init().
17
+ RuntimeLogBuffer.install()
13
18
  UI.init(plugin)
14
19
  local elements = UI.getElements()
15
20
  local toolbar = plugin:CreateToolbar("MCP Integration")
@@ -72,6 +77,7 @@ local HttpService = _services.HttpService
72
77
  local Players = _services.Players
73
78
  local ReplicatedStorage = _services.ReplicatedStorage
74
79
  local RunService = _services.RunService
80
+ local RuntimeLogBuffer = TS.import(script, script.Parent, "RuntimeLogBuffer")
75
81
  -- The client peer cannot reach the MCP HTTP server - Roblox forbids
76
82
  -- HttpService:RequestAsync from the client DM even under PluginSecurity, and
77
83
  -- HttpEnabled reads as false there regardless of identity. So the server peer
@@ -87,6 +93,37 @@ local RunService = _services.RunService
87
93
  -- edit DM untouched.
88
94
  local MCP_URL = "http://localhost:58741"
89
95
  local BROKER_NAME = "__MCPClientBroker"
96
+ -- Endpoints the server-peer broker is allowed to forward to the client peer.
97
+ -- Each requires the client peer's plugin VM (because the buffer / require
98
+ -- cache / etc. lives there) so the server peer alone can't satisfy them.
99
+ local CLIENT_BROKER_ALLOWED_ENDPOINTS = {
100
+ ["/api/execute-luau"] = true,
101
+ ["/api/get-runtime-logs"] = true,
102
+ }
103
+ -- Throttle re-ready calls per proxyId so a brief window of unknownInstance
104
+ -- polls doesn't cause a re-register stampede.
105
+ local lastReadyByProxy = {}
106
+ local postJson
107
+ local function reRegisterProxy(proxyId, role)
108
+ local now = tick()
109
+ local _proxyId = proxyId
110
+ local _condition = lastReadyByProxy[_proxyId]
111
+ if _condition == nil then
112
+ _condition = 0
113
+ end
114
+ local last = _condition
115
+ if now - last < 2 then
116
+ return nil
117
+ end
118
+ local _proxyId_1 = proxyId
119
+ lastReadyByProxy[_proxyId_1] = now
120
+ pcall(function()
121
+ return postJson("/ready", {
122
+ instanceId = proxyId,
123
+ role = role,
124
+ })
125
+ end)
126
+ end
90
127
  local function forkRole()
91
128
  if not RunService:IsRunning() then
92
129
  return "edit"
@@ -96,7 +133,7 @@ local function forkRole()
96
133
  end
97
134
  return "client"
98
135
  end
99
- local function postJson(endpoint, body)
136
+ function postJson(endpoint, body)
100
137
  return pcall(function()
101
138
  return HttpService:RequestAsync({
102
139
  Url = `{MCP_URL}{endpoint}`,
@@ -108,6 +145,56 @@ local function postJson(endpoint, body)
108
145
  })
109
146
  end)
110
147
  end
148
+ local function handleExecuteLuau(data)
149
+ local code = data and (data.code)
150
+ if type(code) == "string" == false or code == "" then
151
+ return {
152
+ success = false,
153
+ error = "code is required",
154
+ }
155
+ end
156
+ local m = Instance.new("ModuleScript")
157
+ m.Name = "__MCPClientEval"
158
+ local okSet, setErr = pcall(function()
159
+ m.Source = code
160
+ end)
161
+ if not okSet then
162
+ m:Destroy()
163
+ return {
164
+ success = false,
165
+ error = `Source set failed: {tostring(setErr)}`,
166
+ }
167
+ end
168
+ m.Parent = game.Workspace
169
+ local okReq, result = pcall(function()
170
+ return require(m)
171
+ end)
172
+ m:Destroy()
173
+ if okReq then
174
+ return {
175
+ success = true,
176
+ returnValue = if result ~= nil then tostring(result) else nil,
177
+ message = "Code executed successfully",
178
+ }
179
+ end
180
+ return {
181
+ success = false,
182
+ error = tostring(result),
183
+ }
184
+ end
185
+ local function handleGetRuntimeLogs(data)
186
+ local d = data or {}
187
+ local since = d.since
188
+ local tail = d.tail
189
+ local filter = d.filter
190
+ -- "client" is the generic peer tag; MCP-side aggregator overrides with
191
+ -- the specific role (e.g. "client-1") on target=all fan-out.
192
+ return RuntimeLogBuffer.query({
193
+ since = since,
194
+ tail = tail,
195
+ filter = filter,
196
+ }, "client")
197
+ end
111
198
  local function setupClientBroker()
112
199
  local rf = ReplicatedStorage:WaitForChild(BROKER_NAME, 10)
113
200
  if not rf or not rf:IsA("RemoteFunction") then
@@ -115,44 +202,20 @@ local function setupClientBroker()
115
202
  return nil
116
203
  end
117
204
  rf.OnClientInvoke = function(payload)
118
- local code = payload and payload.code
119
- if type(code) == "string" == false or code == "" then
120
- return {
121
- success = false,
122
- error = "code is required",
123
- }
205
+ -- Two payload shapes in the wild:
206
+ -- - {endpoint, data} from v2.10+ server-peer broker (this is the new
207
+ -- discriminated form that lets us dispatch on endpoint)
208
+ -- - {code} from pre-v2.10 server-peer broker (raw execute-luau payload)
209
+ -- The shapes coexist gracefully because we fall back to execute-luau
210
+ -- when endpoint is missing.
211
+ if payload and payload.endpoint == "/api/get-runtime-logs" then
212
+ return handleGetRuntimeLogs(payload.data)
124
213
  end
125
- local m = Instance.new("ModuleScript")
126
- m.Name = "__MCPClientEval"
127
- local okSet, setErr = pcall(function()
128
- m.Source = code
129
- end)
130
- if not okSet then
131
- m:Destroy()
132
- local _arg0 = {
133
- success = false,
134
- error = `Source set failed: {tostring(setErr)}`,
135
- }
136
- return _arg0
214
+ if payload and payload.endpoint == "/api/execute-luau" then
215
+ return handleExecuteLuau(payload.data)
137
216
  end
138
- m.Parent = game.Workspace
139
- local okReq, result = pcall(function()
140
- return require(m)
141
- end)
142
- m:Destroy()
143
- if okReq then
144
- local _arg0 = {
145
- success = true,
146
- returnValue = if result ~= nil then tostring(result) else nil,
147
- message = "Code executed successfully",
148
- }
149
- return _arg0
150
- end
151
- local _arg0 = {
152
- success = false,
153
- error = tostring(result),
154
- }
155
- return _arg0
217
+ -- Legacy: raw execute-luau payload at the top level.
218
+ return handleExecuteLuau(payload)
156
219
  end
157
220
  end
158
221
  local proxyByPlayer = {}
@@ -179,33 +242,47 @@ local function pollProxy(proxyId, player, rf)
179
242
  local okJson, body = pcall(function()
180
243
  return HttpService:JSONDecode(res.Body)
181
244
  end)
182
- if okJson and body and body.request and body.requestId ~= nil then
183
- local request = body.request
184
- local response
185
- if request.endpoint == "/api/execute-luau" then
186
- local okInvoke, invokeRes = pcall(function()
187
- return rf:InvokeClient(player, request.data)
188
- end)
189
- if okInvoke then
190
- response = if invokeRes ~= nil then invokeRes else {
191
- success = false,
192
- error = "nil response",
245
+ if okJson and body then
246
+ -- Server lost our proxy registration (process restart, etc.) -
247
+ -- re-register so the next poll cycle starts routing again.
248
+ if body.knownInstance == false then
249
+ reRegisterProxy(proxyId, "client")
250
+ end
251
+ if body.request and body.requestId ~= nil then
252
+ local request = body.request
253
+ local response
254
+ local _endpoint = request.endpoint
255
+ if CLIENT_BROKER_ALLOWED_ENDPOINTS[_endpoint] ~= nil then
256
+ -- Forward as a discriminated envelope so the client-side
257
+ -- OnClientInvoke knows which endpoint it's serving.
258
+ local envelope = {
259
+ endpoint = request.endpoint,
260
+ data = request.data,
193
261
  }
262
+ local okInvoke, invokeRes = pcall(function()
263
+ return rf:InvokeClient(player, envelope)
264
+ end)
265
+ if okInvoke then
266
+ response = if invokeRes ~= nil then invokeRes else {
267
+ success = false,
268
+ error = "nil response",
269
+ }
270
+ else
271
+ response = {
272
+ success = false,
273
+ error = `InvokeClient failed: {tostring(invokeRes)}`,
274
+ }
275
+ end
194
276
  else
195
277
  response = {
196
- success = false,
197
- error = `InvokeClient failed: {tostring(invokeRes)}`,
278
+ error = `Client-proxy does not forward {tostring(request.endpoint)}. ` .. `Allowed: /api/execute-luau, /api/get-runtime-logs.`,
198
279
  }
199
280
  end
200
- else
201
- response = {
202
- error = `Client-proxy only supports /api/execute-luau, got: {tostring(request.endpoint)}`,
203
- }
281
+ postJson("/response", {
282
+ requestId = body.requestId,
283
+ response = response,
284
+ })
204
285
  end
205
- postJson("/response", {
206
- requestId = body.requestId,
207
- response = response,
208
- })
209
286
  end
210
287
  end
211
288
  task.wait(0.5)
@@ -264,22 +341,28 @@ local function startEditProxyLoop()
264
341
  local okJson, body = pcall(function()
265
342
  return HttpService:JSONDecode(pollRes.Body)
266
343
  end)
267
- if okJson and body and body.request and body.request.endpoint == "/api/stop-playtest" and body.requestId ~= nil then
268
- local sts = game:GetService("StudioTestService")
269
- local endOk, endErr = pcall(function()
270
- return sts:EndTest("stopped_by_mcp")
271
- end)
272
- local response = if endOk then {
273
- success = true,
274
- message = "Playtest stopped via edit-proxy/EndTest",
275
- } else {
276
- success = false,
277
- error = `EndTest failed: {tostring(endErr)}`,
278
- }
279
- postJson("/response", {
280
- requestId = body.requestId,
281
- response = response,
282
- })
344
+ if okJson and body then
345
+ -- Re-register if the server lost our edit-proxy registration.
346
+ if body.knownInstance == false then
347
+ reRegisterProxy(proxyId, "edit-proxy")
348
+ end
349
+ if body.request and body.request.endpoint == "/api/stop-playtest" and body.requestId ~= nil then
350
+ local sts = game:GetService("StudioTestService")
351
+ local endOk, endErr = pcall(function()
352
+ return sts:EndTest("stopped_by_mcp")
353
+ end)
354
+ local response = if endOk then {
355
+ success = true,
356
+ message = "Playtest stopped via edit-proxy/EndTest",
357
+ } else {
358
+ success = false,
359
+ error = `EndTest failed: {tostring(endErr)}`,
360
+ }
361
+ postJson("/response", {
362
+ requestId = body.requestId,
363
+ response = response,
364
+ })
365
+ end
283
366
  end
284
367
  end
285
368
  task.wait(0.15)
@@ -351,6 +434,7 @@ local BuildHandlers = TS.import(script, script.Parent, "handlers", "BuildHandler
351
434
  local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandlers")
352
435
  local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
353
436
  local InputHandlers = TS.import(script, script.Parent, "handlers", "InputHandlers")
437
+ local LogHandlers = TS.import(script, script.Parent, "handlers", "LogHandlers")
354
438
  local instanceId = HttpService:GenerateGUID(false)
355
439
  local assignedRole
356
440
  local function detectRole()
@@ -419,6 +503,7 @@ local routeMap = {
419
503
  ["/api/simulate-mouse-input"] = InputHandlers.simulateMouseInput,
420
504
  ["/api/simulate-keyboard-input"] = InputHandlers.simulateKeyboardInput,
421
505
  ["/api/find-and-replace-in-scripts"] = ScriptHandlers.findAndReplaceInScripts,
506
+ ["/api/get-runtime-logs"] = LogHandlers.getRuntimeLogs,
422
507
  }
423
508
  local function processRequest(request)
424
509
  local endpoint = request.endpoint
@@ -460,6 +545,43 @@ local function getConnectionStatus(connIndex)
460
545
  end
461
546
  return "connecting"
462
547
  end
548
+ -- Throttle for re-issuing /ready after the server reports knownInstance=false.
549
+ -- Without this, every poll during the brief window where the server has just
550
+ -- restarted but hasn't seen our re-ready yet would fire a duplicate /ready.
551
+ local lastReadyPostAt = 0
552
+ local function sendReady(conn)
553
+ local now = tick()
554
+ if now - lastReadyPostAt < 2 then
555
+ return nil
556
+ end
557
+ lastReadyPostAt = now
558
+ task.spawn(function()
559
+ local readyOk, readyResult = pcall(function()
560
+ return HttpService:RequestAsync({
561
+ Url = `{conn.serverUrl}/ready`,
562
+ Method = "POST",
563
+ Headers = {
564
+ ["Content-Type"] = "application/json",
565
+ },
566
+ Body = HttpService:JSONEncode({
567
+ instanceId = instanceId,
568
+ role = detectRole(),
569
+ pluginReady = true,
570
+ timestamp = tick(),
571
+ }),
572
+ })
573
+ end)
574
+ if readyOk and readyResult.Success then
575
+ local parseOk, readyData = pcall(function()
576
+ return HttpService:JSONDecode(readyResult.Body)
577
+ end)
578
+ local _value = parseOk and readyData.assignedRole
579
+ if _value ~= "" and _value then
580
+ assignedRole = readyData.assignedRole
581
+ end
582
+ end
583
+ end)
584
+ end
463
585
  local function pollForRequests(connIndex)
464
586
  local conn = State.getConnection(connIndex)
465
587
  if not conn or not conn.isActive then
@@ -489,6 +611,14 @@ local function pollForRequests(connIndex)
489
611
  local mcpConnected = data.mcpConnected == true
490
612
  conn.lastHttpOk = true
491
613
  conn.lastMcpOk = mcpConnected
614
+ -- Server tells us when its in-memory instances map doesn't have us
615
+ -- (e.g. after an MCP process restart). Re-issue /ready immediately so
616
+ -- target=server/client-N start routing again. The throttle inside
617
+ -- sendReady() prevents duplicate registrations while the server
618
+ -- catches up.
619
+ if data.knownInstance == false then
620
+ sendReady(conn)
621
+ end
492
622
  if connIndex == State.getActiveTabIndex() then
493
623
  local el = ui
494
624
  el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
@@ -644,42 +774,19 @@ local function activatePlugin(connIndex)
644
774
  UI.updateUIState()
645
775
  end
646
776
  UI.updateTabDot(idx)
647
- task.spawn(function()
648
- if not conn.heartbeatConnection then
649
- conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
650
- local now = tick()
651
- local currentInterval = if conn.consecutiveFailures > 5 then conn.currentRetryDelay else conn.pollInterval
652
- if now - conn.lastPoll > currentInterval then
653
- conn.lastPoll = now
654
- pollForRequests(idx)
655
- end
656
- end)
657
- end
658
- local readyOk, readyResult = pcall(function()
659
- return HttpService:RequestAsync({
660
- Url = `{conn.serverUrl}/ready`,
661
- Method = "POST",
662
- Headers = {
663
- ["Content-Type"] = "application/json",
664
- },
665
- Body = HttpService:JSONEncode({
666
- instanceId = instanceId,
667
- role = detectRole(),
668
- pluginReady = true,
669
- timestamp = tick(),
670
- }),
671
- })
672
- end)
673
- if readyOk and readyResult.Success then
674
- local parseOk, readyData = pcall(function()
675
- return HttpService:JSONDecode(readyResult.Body)
676
- end)
677
- local _value = parseOk and readyData.assignedRole
678
- if _value ~= "" and _value then
679
- assignedRole = readyData.assignedRole
777
+ if not conn.heartbeatConnection then
778
+ conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
779
+ local now = tick()
780
+ local currentInterval = if conn.consecutiveFailures > 5 then conn.currentRetryDelay else conn.pollInterval
781
+ if now - conn.lastPoll > currentInterval then
782
+ conn.lastPoll = now
783
+ pollForRequests(idx)
680
784
  end
681
- end
682
- end)
785
+ end)
786
+ end
787
+ -- Initial /ready; pollForRequests will also re-fire ready if the server
788
+ -- later reports knownInstance=false (process restart, etc).
789
+ sendReady(conn)
683
790
  end
684
791
  local function deactivatePlugin(connIndex)
685
792
  local _condition = connIndex
@@ -2681,6 +2788,33 @@ return {
2681
2788
  </Properties>
2682
2789
  </Item>
2683
2790
  <Item class="ModuleScript" referent="11">
2791
+ <Properties>
2792
+ <string name="Name">LogHandlers</string>
2793
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
2794
+ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
2795
+ local RuntimeLogBuffer = TS.import(script, script.Parent.Parent, "RuntimeLogBuffer")
2796
+ local function getRuntimeLogs(requestData)
2797
+ local since = requestData.since
2798
+ local tail = requestData.tail
2799
+ local filter = requestData.filter
2800
+ -- Plugin-side peer tag is generic ("edit"|"server"|"client"). The MCP-side
2801
+ -- aggregator overrides it with the specific instance role (e.g. "client-1")
2802
+ -- during fan-out for target=all, so this value is only authoritative for
2803
+ -- the single-peer query path.
2804
+ local peer = RuntimeLogBuffer.detectPeer()
2805
+ return RuntimeLogBuffer.query({
2806
+ since = since,
2807
+ tail = tail,
2808
+ filter = filter,
2809
+ }, peer)
2810
+ end
2811
+ return {
2812
+ getRuntimeLogs = getRuntimeLogs,
2813
+ }
2814
+ ]]></string>
2815
+ </Properties>
2816
+ </Item>
2817
+ <Item class="ModuleScript" referent="12">
2684
2818
  <Properties>
2685
2819
  <string name="Name">MetadataHandlers</string>
2686
2820
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3152,8 +3286,19 @@ local function executeLuau(requestData)
3152
3286
  local runViaModuleScript = function()
3153
3287
  local m = Instance.new("ModuleScript")
3154
3288
  m.Name = "__MCPExecLuauPayload"
3289
+ -- Wrap user code in an IIFE so require() always gets exactly one
3290
+ -- return value. Without this, code like `print("x")` errors with
3291
+ -- "Module code did not return exactly one value" because top-level
3292
+ -- ModuleScripts must return exactly one value.
3293
+ --
3294
+ -- The DOUBLE parens around the call are load-bearing: in Luau,
3295
+ -- `return f()` propagates whatever multi-value tuple f returns,
3296
+ -- including zero values. Outer parens adjust the call to exactly
3297
+ -- one value (the first, or nil). So `return ((f)())` always
3298
+ -- returns exactly one value, regardless of what f does.
3299
+ local wrapped = `return ((function()\n{code}\nend)())`
3155
3300
  local okSet, setErr = pcall(function()
3156
- m.Source = code
3301
+ m.Source = wrapped
3157
3302
  end)
3158
3303
  if not okSet then
3159
3304
  m:Destroy()
@@ -3306,7 +3451,7 @@ return {
3306
3451
  ]]></string>
3307
3452
  </Properties>
3308
3453
  </Item>
3309
- <Item class="ModuleScript" referent="12">
3454
+ <Item class="ModuleScript" referent="13">
3310
3455
  <Properties>
3311
3456
  <string name="Name">PropertyHandlers</string>
3312
3457
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3558,7 +3703,7 @@ return {
3558
3703
  ]]></string>
3559
3704
  </Properties>
3560
3705
  </Item>
3561
- <Item class="ModuleScript" referent="13">
3706
+ <Item class="ModuleScript" referent="14">
3562
3707
  <Properties>
3563
3708
  <string name="Name">QueryHandlers</string>
3564
3709
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -4585,7 +4730,7 @@ return {
4585
4730
  ]]></string>
4586
4731
  </Properties>
4587
4732
  </Item>
4588
- <Item class="ModuleScript" referent="14">
4733
+ <Item class="ModuleScript" referent="15">
4589
4734
  <Properties>
4590
4735
  <string name="Name">ScriptHandlers</string>
4591
4736
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5281,7 +5426,7 @@ return {
5281
5426
  ]]></string>
5282
5427
  </Properties>
5283
5428
  </Item>
5284
- <Item class="ModuleScript" referent="15">
5429
+ <Item class="ModuleScript" referent="16">
5285
5430
  <Properties>
5286
5431
  <string name="Name">TestHandlers</string>
5287
5432
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5600,7 +5745,7 @@ return {
5600
5745
  </Properties>
5601
5746
  </Item>
5602
5747
  </Item>
5603
- <Item class="ModuleScript" referent="16">
5748
+ <Item class="ModuleScript" referent="17">
5604
5749
  <Properties>
5605
5750
  <string name="Name">Recording</string>
5606
5751
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -5630,11 +5775,192 @@ return {
5630
5775
  ]]></string>
5631
5776
  </Properties>
5632
5777
  </Item>
5633
- <Item class="ModuleScript" referent="17">
5778
+ <Item class="ModuleScript" referent="18">
5779
+ <Properties>
5780
+ <string name="Name">RuntimeLogBuffer</string>
5781
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
5782
+ local TS = require(script.Parent.Parent.include.RuntimeLib)
5783
+ -- Per-peer in-memory ring buffer for LogService.MessageOut events.
5784
+ -- Powers the get_runtime_logs MCP tool. Replaces the out-of-tree LogBuffer
5785
+ -- primitives + StringValue approach from chrrxs/roblox-mcp-primitives.
5786
+ --
5787
+ -- Each peer's plugin attaches a MessageOut listener at plugin load (edit DM,
5788
+ -- play-server DM, play-client DM all run their own copy of this module).
5789
+ -- Captured entries live in plugin module-state; nothing is parented to the
5790
+ -- DataModel. The buffer is bounded by a message-byte budget; oldest entries
5791
+ -- drop when over budget.
5792
+ --
5793
+ -- Peer-tag caveat: returned entries reflect which peer's plugin CAPTURED the
5794
+ -- entry, NOT which peer's script originated the print. LogService reflects
5795
+ -- prints across peers in Studio Play (a server print ends up in both the
5796
+ -- server and client LogService:GetLogHistory()) and origin is empirically
5797
+ -- undetectable from inside MessageOut. The MCP-side aggregator handles
5798
+ -- cross-peer dedup via a 2s timestamp window.
5799
+ local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
5800
+ local LogService = _services.LogService
5801
+ local RunService = _services.RunService
5802
+ local MAX_BYTES = 64 * 1024
5803
+ local HARD_ENTRY_CAP = 50_000
5804
+ local entries = {}
5805
+ local totalBytes = 0
5806
+ local totalDropped = 0
5807
+ local nextSeq = 1
5808
+ local installed = false
5809
+ local function levelTag(t)
5810
+ if t == Enum.MessageType.MessageWarning then
5811
+ return "WARN"
5812
+ end
5813
+ if t == Enum.MessageType.MessageError then
5814
+ return "ERR"
5815
+ end
5816
+ if t == Enum.MessageType.MessageInfo then
5817
+ return "INFO"
5818
+ end
5819
+ return "OUT"
5820
+ end
5821
+ local function nowSec()
5822
+ return DateTime.now().UnixTimestampMillis / 1000
5823
+ end
5824
+ local function dropOldestUntilFits(incomingBytes)
5825
+ while #entries > 0 and (totalBytes + incomingBytes > MAX_BYTES or #entries >= HARD_ENTRY_CAP) do
5826
+ local dropped = table.remove(entries, 1)
5827
+ totalBytes -= #dropped.message
5828
+ totalDropped += 1
5829
+ end
5830
+ end
5831
+ local function install()
5832
+ if installed then
5833
+ return nil
5834
+ end
5835
+ if not RunService:IsStudio() then
5836
+ return nil
5837
+ end
5838
+ installed = true
5839
+ LogService.MessageOut:Connect(function(msg, t)
5840
+ local bytes = #msg
5841
+ dropOldestUntilFits(bytes)
5842
+ local _arg0 = {
5843
+ seq = nextSeq,
5844
+ ts = nowSec(),
5845
+ level = levelTag(t),
5846
+ message = msg,
5847
+ }
5848
+ table.insert(entries, _arg0)
5849
+ nextSeq += 1
5850
+ totalBytes += bytes
5851
+ end)
5852
+ end
5853
+ local function detectPeer()
5854
+ if not RunService:IsRunning() then
5855
+ return "edit"
5856
+ end
5857
+ if RunService:IsServer() then
5858
+ return "server"
5859
+ end
5860
+ return "client"
5861
+ end
5862
+ local function query(opts, peer)
5863
+ local _result
5864
+ if opts.since ~= nil then
5865
+ -- ▼ ReadonlyArray.filter ▼
5866
+ local _newValue = {}
5867
+ local _callback = function(e)
5868
+ return e.seq > (opts.since)
5869
+ end
5870
+ local _length = 0
5871
+ for _k, _v in entries do
5872
+ if _callback(_v, _k - 1, entries) == true then
5873
+ _length += 1
5874
+ _newValue[_length] = _v
5875
+ end
5876
+ end
5877
+ -- ▲ ReadonlyArray.filter ▲
5878
+ _result = _newValue
5879
+ else
5880
+ local _array = {}
5881
+ local _length = #_array
5882
+ table.move(entries, 1, #entries, _length + 1, _array)
5883
+ _result = _array
5884
+ end
5885
+ local result = _result
5886
+ if opts.filter ~= nil then
5887
+ -- Plain substring search (4th arg = true). Pattern matching here was
5888
+ -- surprising in practice - Lua magic chars in messages would silently
5889
+ -- not match (e.g. filter="MARK-EDIT" against "MARK-EDIT-001" fails
5890
+ -- because '-' means "0+" in Lua patterns). Substring search matches
5891
+ -- most users' mental model of "filter messages containing this text".
5892
+ local needle = opts.filter
5893
+ -- ▼ ReadonlyArray.filter ▼
5894
+ local _newValue = {}
5895
+ local _callback = function(e)
5896
+ local start = string.find(e.message, needle, 1, true)
5897
+ return start ~= nil
5898
+ end
5899
+ local _length = 0
5900
+ for _k, _v in result do
5901
+ if _callback(_v, _k - 1, result) == true then
5902
+ _length += 1
5903
+ _newValue[_length] = _v
5904
+ end
5905
+ end
5906
+ -- ▲ ReadonlyArray.filter ▲
5907
+ result = _newValue
5908
+ end
5909
+ if opts.tail ~= nil and #result > opts.tail then
5910
+ -- roblox-ts arrays don't expose .slice; manual tail copy.
5911
+ local tailed = {}
5912
+ local start = #result - opts.tail
5913
+ do
5914
+ local i = start
5915
+ local _shouldIncrement = false
5916
+ while true do
5917
+ if _shouldIncrement then
5918
+ i += 1
5919
+ else
5920
+ _shouldIncrement = true
5921
+ end
5922
+ if not (i < #result) then
5923
+ break
5924
+ end
5925
+ local _arg0 = result[i + 1]
5926
+ table.insert(tailed, _arg0)
5927
+ end
5928
+ end
5929
+ result = tailed
5930
+ end
5931
+ local last = if #entries > 0 then entries[#entries] else nil
5932
+ local _object = {
5933
+ peer = peer,
5934
+ entries = result,
5935
+ totalDropped = totalDropped,
5936
+ }
5937
+ local _left = "nextSince"
5938
+ local _result_1
5939
+ if last then
5940
+ _result_1 = last.seq
5941
+ else
5942
+ local _condition = opts.since
5943
+ if _condition == nil then
5944
+ _condition = 0
5945
+ end
5946
+ _result_1 = _condition
5947
+ end
5948
+ _object[_left] = _result_1
5949
+ return _object
5950
+ end
5951
+ return {
5952
+ install = install,
5953
+ detectPeer = detectPeer,
5954
+ query = query,
5955
+ }
5956
+ ]]></string>
5957
+ </Properties>
5958
+ </Item>
5959
+ <Item class="ModuleScript" referent="19">
5634
5960
  <Properties>
5635
5961
  <string name="Name">State</string>
5636
5962
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
5637
- local CURRENT_VERSION = "2.9.1"
5963
+ local CURRENT_VERSION = "2.10.0"
5638
5964
  local MAX_CONNECTIONS = 5
5639
5965
  local BASE_PORT = 58741
5640
5966
  local activeTabIndex = 0
@@ -5726,7 +6052,7 @@ return {
5726
6052
  ]]></string>
5727
6053
  </Properties>
5728
6054
  </Item>
5729
- <Item class="ModuleScript" referent="18">
6055
+ <Item class="ModuleScript" referent="20">
5730
6056
  <Properties>
5731
6057
  <string name="Name">UI</string>
5732
6058
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6446,7 +6772,7 @@ return {
6446
6772
  ]]></string>
6447
6773
  </Properties>
6448
6774
  </Item>
6449
- <Item class="ModuleScript" referent="19">
6775
+ <Item class="ModuleScript" referent="21">
6450
6776
  <Properties>
6451
6777
  <string name="Name">Utils</string>
6452
6778
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -6976,11 +7302,11 @@ return {
6976
7302
  </Properties>
6977
7303
  </Item>
6978
7304
  </Item>
6979
- <Item class="Folder" referent="23">
7305
+ <Item class="Folder" referent="25">
6980
7306
  <Properties>
6981
7307
  <string name="Name">include</string>
6982
7308
  </Properties>
6983
- <Item class="ModuleScript" referent="20">
7309
+ <Item class="ModuleScript" referent="22">
6984
7310
  <Properties>
6985
7311
  <string name="Name">Promise</string>
6986
7312
  <string name="Source"><![CDATA[--[[
@@ -9054,7 +9380,7 @@ return Promise
9054
9380
  ]]></string>
9055
9381
  </Properties>
9056
9382
  </Item>
9057
- <Item class="ModuleScript" referent="21">
9383
+ <Item class="ModuleScript" referent="23">
9058
9384
  <Properties>
9059
9385
  <string name="Name">RuntimeLib</string>
9060
9386
  <string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
@@ -9321,15 +9647,15 @@ return TS
9321
9647
  </Properties>
9322
9648
  </Item>
9323
9649
  </Item>
9324
- <Item class="Folder" referent="24">
9650
+ <Item class="Folder" referent="26">
9325
9651
  <Properties>
9326
9652
  <string name="Name">node_modules</string>
9327
9653
  </Properties>
9328
- <Item class="Folder" referent="25">
9654
+ <Item class="Folder" referent="27">
9329
9655
  <Properties>
9330
9656
  <string name="Name">@rbxts</string>
9331
9657
  </Properties>
9332
- <Item class="ModuleScript" referent="22">
9658
+ <Item class="ModuleScript" referent="24">
9333
9659
  <Properties>
9334
9660
  <string name="Name">services</string>
9335
9661
  <string name="Source"><![CDATA[return setmetatable({}, {