@el4cteo/rbx-studio-mcp 0.3.8 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@el4cteo/rbx-studio-mcp",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for Roblox Studio. 29 tools, push-based SSE bridge, editor-safe script edits, one-step undo.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
 
10
10
  local Config = {}
11
11
 
12
- Config.PLUGIN_VERSION = "0.3.8"
12
+ Config.PLUGIN_VERSION = "0.4.0"
13
13
 
14
14
  -- Fingerprint of plugin/src, stamped in by scripts/build-plugin.mjs. The server
15
15
  -- computes the same hash from its own copy of the sources and compares, so a
@@ -18,12 +18,49 @@
18
18
  Instances, and colour spans do the same job in a fraction of the code.
19
19
  ]]
20
20
 
21
+ local TweenService = game:GetService("TweenService")
22
+
21
23
  local ThemePicker = require(script.Parent.ThemePicker)
22
24
  local Themes = require(script.Parent.Themes)
23
25
  local Visuals = require(script.Parent.Visuals)
24
26
 
25
27
  local Console = {}
26
28
 
29
+ --[[
30
+ The clear wipe: an old CRT being switched off.
31
+
32
+ A button that empties the screen and leaves no trace is indistinguishable
33
+ from one that crashed the panel. The receipt row answers that in words; this
34
+ answers it in the two hundred milliseconds before anyone has read the words.
35
+
36
+ It is an OVERLAY and nothing else. The log is cleared for real at the same
37
+ instant, underneath, so nothing about the console's state waits on an
38
+ animation -- a line that arrives mid-wipe lands in the fresh log and is
39
+ simply revealed when the overlay goes. What collapses is a still copy of the
40
+ text that was on screen, which is the honest thing to animate: it is a
41
+ picture of what was erased.
42
+
43
+ Three phases, and the timings are the whole effect. The picture holds and
44
+ then whips shut to a line; the line sits a moment; the line whips to a point
45
+ and is gone. Easing into each collapse rather than out of it is what makes it
46
+ read as a tube discharging rather than as a panel being resized.
47
+ ]]
48
+ local CRT_COLLAPSE = 0.16
49
+ local CRT_HOLD = 0.07
50
+ local CRT_BLINK = 0.17
51
+ local CRT_SHUT = TweenInfo.new(CRT_COLLAPSE, Enum.EasingStyle.Quart, Enum.EasingDirection.In)
52
+ local CRT_SETTLE = TweenInfo.new(0.05, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
53
+ local CRT_OUT = TweenInfo.new(CRT_BLINK, Enum.EasingStyle.Quart, Enum.EasingDirection.In)
54
+
55
+ --[[
56
+ Explicit depths, because a plugin widget draws with ZIndexBehavior.Global
57
+ and a child does not inherit its parent's. Above the log, the band and the
58
+ hover readout; below the preset drawer, which must stay clickable.
59
+ ]]
60
+ local Z_CRT = 20
61
+ local Z_CRT_TEXT = 21
62
+ local Z_CRT_LINE = 22
63
+
27
64
  --[[
28
65
  Ring buffer bound, counted in logged rows rather than in rendered lines.
29
66
 
@@ -209,6 +246,24 @@ type State = {
209
246
  clients: number,
210
247
  -- Set while a repaint is already scheduled for the end of this frame.
211
248
  dirty: boolean,
249
+
250
+ --[[
251
+ The clear wipe's overlay: an unclipped shell over the log region, the
252
+ collapsing screen inside it, a fixed window holding the still copy of
253
+ the text, and the bright line the picture discharges into.
254
+ ]]
255
+ crt: Frame?,
256
+ crtScreen: Frame?,
257
+ crtWindow: Frame?,
258
+ crtGhost: TextLabel?,
259
+ crtLine: Frame?,
260
+ --[[
261
+ Bumped by every wipe, so the delayed halves of an earlier one know they
262
+ have been superseded and do nothing. `task.delay` hands back no handle to
263
+ cancel, and clearing twice inside four hundred milliseconds is a click
264
+ away.
265
+ ]]
266
+ crtGeneration: number,
212
267
  }
213
268
 
214
269
  local state: State = {
@@ -232,6 +287,12 @@ local state: State = {
232
287
  generation = 0,
233
288
  clients = 1,
234
289
  dirty = false,
290
+ crt = nil,
291
+ crtScreen = nil,
292
+ crtWindow = nil,
293
+ crtGhost = nil,
294
+ crtLine = nil,
295
+ crtGeneration = 0,
235
296
  }
236
297
 
237
298
  -- How long the session must be silent before the console says so. Long enough
@@ -656,7 +717,121 @@ function Console.setClients(count: number)
656
717
  end
657
718
  end
658
719
 
720
+ --[[
721
+ Plays the wipe over whatever is on screen right now.
722
+
723
+ Must be called BEFORE the log is emptied: the still copy it collapses is
724
+ read straight off the live label, scroll offset and all, so what discharges
725
+ is exactly the text the user was looking at rather than a re-render of it.
726
+
727
+ Silent when there is nothing to play it on -- a widget dragged down to a
728
+ sliver has no room for a picture to collapse, and a flourish that plays in
729
+ four pixels is a flicker, not an effect.
730
+ ]]
731
+ local function crtWipe()
732
+ local shell = state.crt
733
+ local screen = state.crtScreen
734
+ local window = state.crtWindow
735
+ local ghost = state.crtGhost
736
+ local line = state.crtLine
737
+ local scroller = state.scroller
738
+ local label = state.label
739
+ if
740
+ shell == nil
741
+ or screen == nil
742
+ or window == nil
743
+ or ghost == nil
744
+ or line == nil
745
+ or scroller == nil
746
+ or label == nil
747
+ then
748
+ return
749
+ end
750
+
751
+ local height = scroller.AbsoluteSize.Y
752
+ if height < 24 then
753
+ return
754
+ end
755
+
756
+ state.crtGeneration += 1
757
+ local generation = state.crtGeneration
758
+
759
+ -- Read at play time rather than tracked, so the overlay lands on the log
760
+ -- wherever the band toggle and the widget's size have left it.
761
+ shell.Position = scroller.Position
762
+ shell.Size = scroller.Size
763
+
764
+ --[[
765
+ The still copy, offset by exactly as far as the log is scrolled.
766
+
767
+ A TextLabel cannot scroll its own text, so the window is a fixed-height
768
+ clip and the label inside it is pushed up by the canvas offset. That is
769
+ what makes the copy line up with the real log to the pixel instead of
770
+ snapping to the top of the buffer the moment the wipe starts.
771
+ ]]
772
+ window.Size = UDim2.new(1, 0, 0, height)
773
+ ghost.Position = UDim2.new(0, 12, 0, 8 - scroller.CanvasPosition.Y)
774
+ ghost.Size = UDim2.new(1, -24, 0, 0)
775
+ ghost.TextColor3 = PALETTE.text
776
+ ghost.Text = label.Text
777
+
778
+ screen.BackgroundColor3 = PALETTE.background
779
+ screen.Size = UDim2.new(1, 0, 1, 0)
780
+ screen.Visible = true
781
+
782
+ line.BackgroundColor3 = PALETTE.text
783
+ line.Size = UDim2.new(1, 0, 0, 2)
784
+ line.BackgroundTransparency = 1
785
+ line.Visible = false
786
+
787
+ shell.Visible = true
788
+
789
+ -- The picture, whipping shut around its own middle.
790
+ TweenService:Create(screen, CRT_SHUT, { Size = UDim2.new(1, 0, 0, 2) }):Play()
791
+
792
+ task.delay(CRT_COLLAPSE, function()
793
+ if state.crtGeneration ~= generation then
794
+ return
795
+ end
796
+
797
+ --[[
798
+ The handover. The screen goes and the line arrives in the same frame,
799
+ a little over-thick, and settles -- which is the flash. Fading one
800
+ into the other instead just looks like a crossfade of two rectangles.
801
+ ]]
802
+ screen.Visible = false
803
+ line.BackgroundTransparency = 0
804
+ line.Size = UDim2.new(1, 0, 0, 3)
805
+ line.Visible = true
806
+ TweenService:Create(line, CRT_SETTLE, { Size = UDim2.new(1, 0, 0, 2) }):Play()
807
+
808
+ task.delay(CRT_HOLD, function()
809
+ if state.crtGeneration ~= generation then
810
+ return
811
+ end
812
+
813
+ TweenService:Create(line, CRT_OUT, {
814
+ Size = UDim2.new(0, 0, 0, 2),
815
+ BackgroundTransparency = 0.35,
816
+ }):Play()
817
+
818
+ task.delay(CRT_BLINK, function()
819
+ if state.crtGeneration ~= generation then
820
+ return
821
+ end
822
+ shell.Visible = false
823
+ line.Visible = false
824
+ screen.Visible = true
825
+ end)
826
+ end)
827
+ end)
828
+ end
829
+
659
830
  function Console.clear()
831
+ -- Before anything is emptied: the wipe collapses a copy of what is on
832
+ -- screen, and a moment later there is nothing on screen to copy.
833
+ crtWipe()
834
+
660
835
  --[[
661
836
  Says what it did, with a timestamp, like everything else in here.
662
837
 
@@ -691,6 +866,9 @@ end
691
866
  visible without scrolling, however long the session has run.
692
867
  ]]
693
868
  function Console.setStatus(status: string, meta: string)
869
+ -- Read before it is overwritten: the arrival flourish below is the one thing
870
+ -- here that cares whether this is a change or a repeat.
871
+ local previous = state.status
694
872
  state.status = status
695
873
  state.statusMeta = meta
696
874
 
@@ -718,6 +896,23 @@ function Console.setStatus(status: string, meta: string)
718
896
  -- The wireframe takes the same colour as the status light, so the panel
719
897
  -- reads as disconnected at a glance even with the header off screen.
720
898
  Visuals.setTint(color)
899
+
900
+ -- The trace has no calls to plot until a session exists, so it waves while
901
+ -- the transport is reaching for one rather than sitting as a dead baseline
902
+ -- at exactly the moment somebody is watching to see whether it is alive.
903
+ Visuals.setConnecting(status == "connecting")
904
+
905
+ --[[
906
+ And it celebrates when the session finally lands.
907
+
908
+ Only on the transition. `setStatus` is also how a theme switch and a
909
+ port change repaint the header, and both replay the current status --
910
+ which would fire the flourish again for an event that already happened,
911
+ several times a session, until it read as noise rather than as news.
912
+ ]]
913
+ if status == "connected" and previous ~= "connected" then
914
+ Visuals.celebrate()
915
+ end
721
916
  end
722
917
 
723
918
  --[[
@@ -958,6 +1153,84 @@ function Console.mount(parent: Instance, handlers: Handlers)
958
1153
  label.Parent = scroller
959
1154
  state.label = label
960
1155
 
1156
+ --[[
1157
+ The clear wipe's overlay, built once and hidden.
1158
+
1159
+ Four nested pieces, and each one earns its place. `crt` is unclipped and
1160
+ covers the log region, so the line can stay full width while the screen
1161
+ inside it closes. `screen` is the clip that actually collapses, anchored
1162
+ to its own middle so it shuts toward the centre rather than rolling up
1163
+ from the top. `window` is a fixed-height clip that does not move with it,
1164
+ which is what holds the copied text still while the screen closes over
1165
+ it. `ghost` is the copy.
1166
+ ]]
1167
+ local crt = Instance.new("Frame")
1168
+ crt.Name = "ClearWipe"
1169
+ crt.BackgroundTransparency = 1
1170
+ crt.BorderSizePixel = 0
1171
+ crt.Visible = false
1172
+ crt.ZIndex = Z_CRT
1173
+ crt.Parent = root
1174
+ state.crt = crt
1175
+
1176
+ local crtScreen = Instance.new("Frame")
1177
+ crtScreen.Name = "Screen"
1178
+ crtScreen.AnchorPoint = Vector2.new(0.5, 0.5)
1179
+ crtScreen.Position = UDim2.fromScale(0.5, 0.5)
1180
+ crtScreen.Size = UDim2.new(1, 0, 1, 0)
1181
+ themed(crtScreen, "BackgroundColor3", "background")
1182
+ crtScreen.BorderSizePixel = 0
1183
+ crtScreen.ClipsDescendants = true
1184
+ crtScreen.ZIndex = Z_CRT
1185
+ crtScreen.Parent = crt
1186
+ state.crtScreen = crtScreen
1187
+
1188
+ local crtWindow = Instance.new("Frame")
1189
+ crtWindow.Name = "Window"
1190
+ crtWindow.AnchorPoint = Vector2.new(0.5, 0.5)
1191
+ crtWindow.Position = UDim2.fromScale(0.5, 0.5)
1192
+ crtWindow.Size = UDim2.new(1, 0, 1, 0)
1193
+ crtWindow.BackgroundTransparency = 1
1194
+ crtWindow.BorderSizePixel = 0
1195
+ crtWindow.ClipsDescendants = true
1196
+ crtWindow.ZIndex = Z_CRT_TEXT
1197
+ crtWindow.Parent = crtScreen
1198
+ state.crtWindow = crtWindow
1199
+
1200
+ -- Every text property the log's own label has, because the copy has to be
1201
+ -- indistinguishable from it for the frame before it starts moving.
1202
+ local crtGhost = Instance.new("TextLabel")
1203
+ crtGhost.Name = "Ghost"
1204
+ crtGhost.BackgroundTransparency = 1
1205
+ crtGhost.Font = Enum.Font.Code
1206
+ crtGhost.TextSize = 12
1207
+ crtGhost.LineHeight = 1.25
1208
+ themed(crtGhost, "TextColor3", "text")
1209
+ crtGhost.RichText = true
1210
+ crtGhost.TextWrapped = true
1211
+ crtGhost.AutomaticSize = Enum.AutomaticSize.Y
1212
+ crtGhost.Size = UDim2.new(1, -24, 0, 0)
1213
+ crtGhost.TextXAlignment = Enum.TextXAlignment.Left
1214
+ crtGhost.TextYAlignment = Enum.TextYAlignment.Top
1215
+ crtGhost.Text = ""
1216
+ crtGhost.ZIndex = Z_CRT_TEXT
1217
+ crtGhost.Parent = crtWindow
1218
+ state.crtGhost = crtGhost
1219
+
1220
+ -- The line the picture discharges into. Outside `screen`, so it keeps its
1221
+ -- width while the screen closes to nothing behind it.
1222
+ local crtLine = Instance.new("Frame")
1223
+ crtLine.Name = "Line"
1224
+ crtLine.AnchorPoint = Vector2.new(0.5, 0.5)
1225
+ crtLine.Position = UDim2.fromScale(0.5, 0.5)
1226
+ crtLine.Size = UDim2.new(1, 0, 0, 2)
1227
+ themed(crtLine, "BackgroundColor3", "text")
1228
+ crtLine.BorderSizePixel = 0
1229
+ crtLine.Visible = false
1230
+ crtLine.ZIndex = Z_CRT_LINE
1231
+ crtLine.Parent = crt
1232
+ state.crtLine = crtLine
1233
+
961
1234
  -- Status bar ------------------------------------------------------------
962
1235
  local footer = Instance.new("Frame")
963
1236
  footer.AnchorPoint = Vector2.new(0, 1)