pulse-rb 1.4.0 → 1.4.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.
@@ -184,6 +184,9 @@ function _UIAdapter:CreateWindow(title, w, h, opts)
184
184
  local tabProxy = { _wind = windTab }
185
185
  function tabProxy:AddLeftGroupbox(title, gbIcon) return _mkSection(leftCol, title, gbIcon) end
186
186
  function tabProxy:AddRightGroupbox(title, gbIcon) return _mkSection(rightCol, title, gbIcon) end
187
+ -- Raw column access — no Section wrapper (used by plain=true groupboxes)
188
+ function tabProxy:GetLeftColumn() return { _container = leftCol } end
189
+ function tabProxy:GetRightColumn() return { _container = rightCol } end
187
190
  return tabProxy
188
191
  end
189
192
 
@@ -587,14 +590,25 @@ task.spawn(function()
587
590
  local _execVer = ""
588
591
  pcall(function()
589
592
  if type(rawget(_G, "identifyexecutor")) == "function" then
590
- local n, v = identifyexecutor()
591
- _execName = tostring(n or "Unknown")
592
- _execVer = tostring(v or "")
593
- elseif rawget(_G, "syn") then _execName = "Synapse X"
594
- elseif rawget(_G, "KRNL_LOADED") then _execName = "Krnl"
595
- elseif rawget(_G, "Delta") then _execName = "Delta"
596
- elseif rawget(_G, "is_sirhurt_closure") then _execName = "Sirhurt"
597
- elseif rawget(_G, "EXECUTOR_NAME") then _execName = tostring(rawget(_G, "EXECUTOR_NAME"))
593
+ local ok, n, v = pcall(identifyexecutor)
594
+ if ok then _execName = tostring(n or "Unknown"); _execVer = tostring(v or "") end
595
+ end
596
+ if _execName == "Unknown" and type(rawget(_G, "getexecutorname")) == "function" then
597
+ local ok, n = pcall(getexecutorname)
598
+ if ok and n then _execName = tostring(n) end
599
+ end
600
+ if _execName == "Unknown" then
601
+ if rawget(_G, "syn") or rawget(_G, "Synapse") then _execName = "Synapse X"
602
+ elseif rawget(_G, "KRNL_LOADED") then _execName = "Krnl"
603
+ elseif rawget(_G, "DELTA_KEY") then _execName = "Delta"
604
+ elseif rawget(_G, "WAVE_LOADED") then _execName = "Wave"
605
+ elseif rawget(_G, "SOLARA_LOADED") then _execName = "Solara"
606
+ elseif rawget(_G, "XENO_LOADED") then _execName = "Xeno"
607
+ elseif rawget(_G, "Seliware") or rawget(_G, "seliware") then _execName = "Seliware"
608
+ elseif rawget(_G, "Celery") or rawget(_G, "celery") then _execName = "Celery"
609
+ elseif rawget(_G, "is_sirhurt_closure") then _execName = "Sirhurt"
610
+ elseif rawget(_G, "EXECUTOR_NAME") then _execName = tostring(rawget(_G, "EXECUTOR_NAME"))
611
+ end
598
612
  end
599
613
  end)
600
614
 
@@ -605,84 +619,131 @@ task.spawn(function()
605
619
  if info and info.Name then _gameName = info.Name end
606
620
  end)
607
621
 
608
- local _homeTab = _win:AddTab("Home", "house")
609
-
610
- -- Left column ──────────────────────────────────────────────────────────────
611
-
612
- -- Welcome / player section
613
- local _welcomeGb = _homeTab:AddLeftGroupbox("Welcome", "person")
614
- if _avatarUrl ~= "" then
615
- -- WindUI Image element silently skipped if unsupported
622
+ -- Home tab — raw columns, no Section wrappers for a clean look.
623
+ -- Create the tab directly so we can get the raw VStack columns from _mkColumns.
624
+ local _homeWindTab
625
+ pcall(function() _homeWindTab = _windWindow:Tab({ Title = "Home", Icon = "house" }) end)
626
+ if not _UIAdapter._firstTab then _UIAdapter._firstTab = _homeWindTab end
627
+ local _homeLeft, _homeRight = _mkColumns(_homeWindTab)
628
+
629
+ -- Left: player info + description + discord (no Section box)
630
+ if _homeLeft then
631
+ -- Capture the returned paragraph element so we can try avatar injection via its Frame
632
+ local _paraRef = nil
616
633
  pcall(function()
617
- _welcomeGb._container:Image({
618
- Source = _avatarUrl,
619
- Size = UDim2.fromOffset(96, 96),
634
+ _paraRef = _homeLeft:Paragraph({
635
+ Title = "Welcome, " .. _dspName,
636
+ Desc = "@" .. _usrName,
620
637
  })
621
638
  end)
622
- end
623
- _welcomeGb._container:Paragraph({
624
- Title = _dspName,
625
- Desc = "@" .. _usrName,
626
- })
627
639
 
628
- if L.description and L.description ~= "" then
629
- _welcomeGb._container:Divider()
630
- _welcomeGb._container:Paragraph({ Title = "", Desc = L.description })
631
- end
640
+ -- Avatar injection: WindUI paragraph elements wrap a Roblox Frame internally.
641
+ -- Grab that Frame's parent (the list container) and add an ImageLabel there.
642
+ if _avatarUrl ~= "" and _paraRef then
643
+ task.delay(0.3, function()
644
+ pcall(function()
645
+ local paraFrame = nil
646
+ for _, k in ipairs({"Frame","Object","Instance","Container","Label","Content"}) do
647
+ local ok, v = pcall(function() return _paraRef[k] end)
648
+ if ok and type(v) == "userdata" then
649
+ local _, isGui = pcall(function() return v:IsA("GuiObject") end)
650
+ if isGui then paraFrame = v; break end
651
+ end
652
+ end
653
+ local contentFrame = paraFrame and paraFrame.Parent
654
+ if not (contentFrame and type(contentFrame) == "userdata") then return end
655
+ if contentFrame:FindFirstChild("_PulseAvatar") then return end
656
+ local img = Instance.new("ImageLabel")
657
+ img.Name = "_PulseAvatar"; img.Image = _avatarUrl
658
+ img.Size = UDim2.fromOffset(64, 64); img.BackgroundTransparency = 1
659
+ img.ScaleType = Enum.ScaleType.Crop; img.LayoutOrder = -999
660
+ Instance.new("UICorner", img).CornerRadius = UDim.new(1, 0)
661
+ img.Parent = contentFrame
662
+ end)
663
+ end)
664
+ end
632
665
 
633
- if L.discord and L.discord ~= "" then
634
- local _discGb = _homeTab:AddLeftGroupbox("Community", "message-circle")
635
- _discGb._container:Button({
636
- Title = "Join Discord",
637
- Desc = "Tap to copy the invite link",
638
- Callback = function()
639
- local ok = false
640
- if not ok then pcall(function() setclipboard(L.discord); ok=true end) end
641
- if not ok then pcall(function() toclipboard(L.discord); ok=true end) end
642
- if not ok then pcall(function() writeclipboard(L.discord); ok=true end) end
643
- _PulseNotify(ok and "Discord link copied!" or "No clipboard API available", 3)
644
- end,
645
- })
666
+ if L.description and L.description ~= "" then
667
+ pcall(function() _homeLeft:Divider() end)
668
+ pcall(function() _homeLeft:Paragraph({ Title = "About", Desc = L.description }) end)
669
+ end
670
+ if L.discord and L.discord ~= "" then
671
+ pcall(function() _homeLeft:Divider() end)
672
+ pcall(function()
673
+ _homeLeft:Button({
674
+ Title = "Join Discord",
675
+ Desc = "Tap to copy the invite link",
676
+ Callback = function()
677
+ local ok = false
678
+ if not ok then pcall(function() setclipboard(L.discord); ok=true end) end
679
+ if not ok then pcall(function() toclipboard(L.discord); ok=true end) end
680
+ if not ok then pcall(function() writeclipboard(L.discord); ok=true end) end
681
+ _PulseNotify(ok and "Discord link copied!" or "No clipboard API", 3)
682
+ end,
683
+ })
684
+ end)
685
+ end
646
686
  end
647
687
 
648
- -- Right column ─────────────────────────────────────────────────────────────
649
-
650
- -- Script metadata
651
- local _scriptGb = _homeTab:AddRightGroupbox("Script", "code-2")
652
- local _verStr = (L.version and L.version ~= "") and ("v" .. L.version) or ""
653
- local _authStr = (L.author and L.author ~= "") and ("by " .. L.author) or ""
654
- local _metaParts = {}
655
- if _verStr ~= "" then _metaParts[#_metaParts+1] = _verStr end
656
- if _authStr ~= "" then _metaParts[#_metaParts+1] = _authStr end
657
- _scriptGb._container:Paragraph({
658
- Title = L.title or "Hub",
659
- Desc = #_metaParts > 0 and table.concat(_metaParts, " · ") or "—",
660
- })
661
-
662
- -- System info
663
- local _sysGb = _homeTab:AddRightGroupbox("System", "monitor")
664
- _sysGb._container:Paragraph({
665
- Title = "Executor",
666
- Desc = _execName .. (_execVer ~= "" and (" " .. _execVer) or ""),
667
- })
668
- _sysGb._container:Paragraph({ Title = "Game", Desc = _gameName })
669
-
670
- -- Unsupported executors (optional — set L.unsupported in layout config)
671
- if L.unsupported and L.unsupported ~= "" then
672
- local _unsupList = {}
673
- if type(L.unsupported) == "table" then
674
- for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
675
- elseif type(L.unsupported) == "string" then
676
- for part in L.unsupported:gmatch("[^,]+") do
677
- _unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
678
- end
679
- end
680
- if #_unsupList > 0 then
681
- local _unsupGb = _homeTab:AddRightGroupbox("Not Supported", "alert-triangle")
682
- _unsupGb._container:Paragraph({
683
- Title = "Not compatible with:",
684
- Desc = table.concat(_unsupList, ", "),
688
+ -- Right: script info + executor + game (no Section box)
689
+ if _homeRight then
690
+ local _verStr = (L.version and L.version ~= "") and ("v" .. L.version) or ""
691
+ local _authStr = (L.author and L.author ~= "") and ("by " .. L.author) or ""
692
+ local _metaParts = {}
693
+ if _verStr ~= "" then _metaParts[#_metaParts+1] = _verStr end
694
+ if _authStr ~= "" then _metaParts[#_metaParts+1] = _authStr end
695
+ pcall(function()
696
+ _homeRight:Paragraph({
697
+ Title = L.title or "Hub",
698
+ Desc = #_metaParts > 0 and table.concat(_metaParts, " · ") or "",
699
+ })
700
+ end)
701
+ pcall(function() _homeRight:Divider() end)
702
+ local _execStr = _execName .. (_execVer ~= "" and (" " .. _execVer) or "")
703
+ pcall(function() _homeRight:Paragraph({ Title = "Executor", Desc = _execStr }) end)
704
+ pcall(function() _homeRight:Paragraph({ Title = "Game", Desc = _gameName }) end)
705
+ pcall(function() _homeRight:Divider() end)
706
+ -- Server info: live player count + rejoin
707
+ local _playerCount = 0
708
+ pcall(function() _playerCount = #game:GetService("Players"):GetPlayers() end)
709
+ pcall(function()
710
+ _homeRight:Paragraph({
711
+ Title = "Server",
712
+ Desc = tostring(_playerCount) .. " player" .. (_playerCount == 1 and "" or "s") .. " · " .. tostring(game.PlaceId),
713
+ })
714
+ end)
715
+ pcall(function()
716
+ _homeRight:Button({
717
+ Title = "Rejoin Server",
718
+ Desc = "Leave and rejoin the current place",
719
+ Callback = function()
720
+ pcall(function()
721
+ game:GetService("TeleportService"):TeleportToPlaceInstance(
722
+ game.PlaceId, game.JobId,
723
+ game:GetService("Players").LocalPlayer
724
+ )
725
+ end)
726
+ end,
685
727
  })
728
+ end)
729
+ if L.unsupported and L.unsupported ~= "" then
730
+ local _unsupList = {}
731
+ if type(L.unsupported) == "table" then
732
+ for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
733
+ elseif type(L.unsupported) == "string" then
734
+ for part in L.unsupported:gmatch("[^,]+") do
735
+ _unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
736
+ end
737
+ end
738
+ if #_unsupList > 0 then
739
+ pcall(function() _homeRight:Divider() end)
740
+ pcall(function()
741
+ _homeRight:Paragraph({
742
+ Title = "Not compatible with:",
743
+ Desc = table.concat(_unsupList, ", "),
744
+ })
745
+ end)
746
+ end
686
747
  end
687
748
  end
688
749
 
@@ -691,7 +752,14 @@ task.spawn(function()
691
752
  local tab = _win:AddTab(page.title, page.icon)
692
753
  for _, gb in ipairs(page.layout or {}) do
693
754
  local container
694
- if gb.side == "left" then
755
+ if gb.plain then
756
+ -- No Section wrapper — widgets go directly onto the column VStack
757
+ if gb.side == "left" then
758
+ if tab.GetLeftColumn then container = tab:GetLeftColumn() end
759
+ else
760
+ if tab.GetRightColumn then container = tab:GetRightColumn() end
761
+ end
762
+ elseif gb.side == "left" then
695
763
  container = tab:AddLeftGroupbox(gb.title, gb.icon)
696
764
  else
697
765
  container = tab:AddRightGroupbox(gb.title, gb.icon)
@@ -784,6 +852,19 @@ task.spawn(function()
784
852
  key = _PULSE_TOGGLE_KEY,
785
853
  action = function() _UIAdapter:ToggleWindow() end,
786
854
  })
855
+ gb_menu._container:Button({
856
+ Title = "Rejoin",
857
+ Icon = "refresh-cw",
858
+ Desc = "Leave and rejoin the current place",
859
+ Callback = function()
860
+ pcall(function()
861
+ game:GetService("TeleportService"):TeleportToPlaceInstance(
862
+ game.PlaceId, game.JobId,
863
+ game:GetService("Players").LocalPlayer
864
+ )
865
+ end)
866
+ end,
867
+ })
787
868
 
788
869
  -- Auto-load saved config on startup
789
870
  task.spawn(function()
@@ -799,14 +880,19 @@ task.spawn(function()
799
880
  local _devTab
800
881
  pcall(function() _devTab = _windWindow:Tab({ Title = "Dev", Icon = "bug" }) end)
801
882
  if _devTab then
802
- local _devCol
803
- pcall(function() _devCol = _devTab:VStack({}) end)
804
- _devCol = _devCol or _devTab
883
+ local _devLeft, _devRight
884
+ pcall(function()
885
+ local _devHs = _devTab:HStack({ AutoSpace = true })
886
+ _devLeft = _devHs:VStack({})
887
+ _devRight = _devHs:VStack({})
888
+ end)
889
+ _devLeft = _devLeft or _devTab
890
+ _devRight = _devRight or _devTab
805
891
 
806
- -- Tools section
892
+ -- Tools section (left)
807
893
  local _toolSect
808
894
  pcall(function()
809
- _toolSect = _devCol:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
895
+ _toolSect = _devLeft:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
810
896
  end)
811
897
 
812
898
  local _TOOLS = {
@@ -864,7 +950,7 @@ task.spawn(function()
864
950
  -- Scanner section
865
951
  local _scanSect
866
952
  pcall(function()
867
- _scanSect = _devCol:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
953
+ _scanSect = _devRight:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
868
954
  end)
869
955
 
870
956
  if _scanSect then
@@ -976,7 +1062,7 @@ task.spawn(function()
976
1062
  -- Info section — framework version + signal counts for debugging
977
1063
  local _infoSect
978
1064
  pcall(function()
979
- _infoSect = _devCol:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
1065
+ _infoSect = _devLeft:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
980
1066
  end)
981
1067
  if _infoSect then
982
1068
  local nPages = #_BundlePages
@@ -1000,6 +1086,47 @@ task.spawn(function()
1000
1086
  end
1001
1087
  end
1002
1088
 
1089
+ -- Heartbeat Monitor section — tick counts per component (left)
1090
+ -- Uses Pulse.Monitor which is independent of the log system.
1091
+ local _monSect
1092
+ pcall(function()
1093
+ _monSect = _devLeft:Section({ Title="Heartbeat Monitor", Icon="activity", Box=true, BoxBorder=true, Opened=true })
1094
+ end)
1095
+ if _monSect then
1096
+ local _monPara
1097
+ pcall(function()
1098
+ _monPara = _monSect:Paragraph({
1099
+ Title = "raw / cond (updates 2s)",
1100
+ Desc = "raw = every HB fire · cond = when() passed",
1101
+ })
1102
+ end)
1103
+ -- Reads _rawHb/_condHb plain-Lua counters set in runtime._hbBind.
1104
+ -- No dependency on Pulse.Monitor — works regardless of log system state.
1105
+ task.spawn(function()
1106
+ while _windWindow do
1107
+ task.wait(2)
1108
+ pcall(function()
1109
+ local lines = {}
1110
+ for cname, comp in pairs(_BundleComponents) do
1111
+ if type(comp) == "table" and comp._rawHb ~= nil then
1112
+ lines[#lines+1] = cname
1113
+ .. " raw=" .. tostring(comp._rawHb)
1114
+ .. " cond=" .. tostring(comp._condHb)
1115
+ end
1116
+ end
1117
+ if _monPara then
1118
+ if #lines == 0 then
1119
+ _monPara:SetDesc("No heartbeat components registered")
1120
+ else
1121
+ table.sort(lines)
1122
+ _monPara:SetDesc(table.concat(lines, "\n"))
1123
+ end
1124
+ end
1125
+ end)
1126
+ end
1127
+ end)
1128
+ end
1129
+
1003
1130
  -- Console section — controls for the Roblox developer console (F9).
1004
1131
  -- "Clear" wipes the output so only your next prints are visible.
1005
1132
  -- "Print Logs" re-dumps all buffered Pulse.Log entries so you can read
@@ -1007,7 +1134,7 @@ task.spawn(function()
1007
1134
  -- "Copy Logs" copies the full log buffer to clipboard.
1008
1135
  local _consoleSect
1009
1136
  pcall(function()
1010
- _consoleSect = _devCol:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
1137
+ _consoleSect = _devRight:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
1011
1138
  end)
1012
1139
  if _consoleSect and _BundlePulse and _BundlePulse.Log then
1013
1140
  local _Log = _BundlePulse.Log
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ var RB_VERSION, RB_HOME, IRONBREW2_DIR, IRONBREW2_REPO, ALWAYS_EXCLUDE, VALID_UI
56
56
  var init_constants = __esm({
57
57
  "src/constants.ts"() {
58
58
  init_cjs_shims();
59
- RB_VERSION = "1.4.0";
59
+ RB_VERSION = "1.4.1";
60
60
  RB_HOME = path.join(os.homedir(), ".rb");
61
61
  path.join(RB_HOME, "bin");
62
62
  IRONBREW2_DIR = path.join(RB_HOME, "ironbrew2");
@@ -1373,6 +1373,21 @@ end)
1373
1373
  }
1374
1374
  parts.push(`for _k,_v in pairs(_P) do _G[_k]=_v end
1375
1375
  `);
1376
+ parts.push(
1377
+ `do
1378
+ local _rs=game:GetService("RunService")
1379
+ local _lp_=game:GetService("Players").LocalPlayer
1380
+ local _uis_=game:GetService("UserInputService")
1381
+ _rs.Heartbeat:Connect(function(a,b) if _PulseRunHeartbeat then _PulseRunHeartbeat(a,b) end end)
1382
+ _rs.RenderStepped:Connect(function(a,b) if _PulseRunRenderStepped then _PulseRunRenderStepped(a,b) end end)
1383
+ _rs.Stepped:Connect(function(a,b) if _PulseRunStepped then _PulseRunStepped(a,b) end end)
1384
+ _lp_.CharacterAdded:Connect(function(c) if _PulseRunCharAdded then _PulseRunCharAdded(c) end end)
1385
+ _lp_.CharacterRemoving:Connect(function(c) if _PulseRunCharRemoving then _PulseRunCharRemoving(c) end end)
1386
+ _uis_.InputBegan:Connect(function(i,g) if _PulseRunInputBegan then _PulseRunInputBegan(i,g) end end)
1387
+ _uis_.InputEnded:Connect(function(i,g) if _PulseRunInputEnded then _PulseRunInputEnded(i,g) end end)
1388
+ end
1389
+ `
1390
+ );
1376
1391
  parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))(_P,_PULSE_LAYOUT)
1377
1392
  `);
1378
1393
  parts.push(`local signal,computed,defineComponent,on=_P.signal,_P.computed,_P.defineComponent,_P.on
@@ -2992,7 +3007,7 @@ async function cmdPublish(_args) {
2992
3007
  }
2993
3008
  }
2994
3009
  bundleParts.push(
2995
- "return {signal=signal,computed=computed,defineComponent=defineComponent,on=on,toggle=toggle,slider=slider,dropdown=dropdown,multidropdown=multidropdown,button=button,keybind=keybind,label=label,separator=separator,groupbox=groupbox,definePage=definePage,Pulse=Pulse,Signal=Signal,Computed=Computed,Component=Component,Components=Components,Store=Store,PulseEvent=PulseEvent,_PulseGetChar=_PulseGetChar,_PulseGetHRP=_PulseGetHRP,_PulseGetHumanoid=_PulseGetHumanoid,_PulseGetAlive=_PulseGetAlive,_PulseRS=_PulseRS,_PulseUIS=_PulseUIS,_PulseDestroy=_PulseDestroy,_PULSE_DEFAULTS=_PULSE_DEFAULTS,_Pages=_Pages}"
3010
+ "return {signal=signal,computed=computed,defineComponent=defineComponent,on=on,toggle=toggle,slider=slider,dropdown=dropdown,multidropdown=multidropdown,button=button,keybind=keybind,label=label,separator=separator,groupbox=groupbox,definePage=definePage,Pulse=Pulse,Signal=Signal,Computed=Computed,Component=Component,Components=Components,Store=Store,PulseEvent=PulseEvent,_PulseGetChar=_PulseGetChar,_PulseGetHRP=_PulseGetHRP,_PulseGetHumanoid=_PulseGetHumanoid,_PulseGetAlive=_PulseGetAlive,_PulseRS=_PulseRS,_PulseUIS=_PulseUIS,_PulseDestroy=_PulseDestroy,_PULSE_DEFAULTS=_PULSE_DEFAULTS,_Pages=_Pages,_PulseRunHeartbeat=_PulseRunHeartbeat,_PulseRunRenderStepped=_PulseRunRenderStepped,_PulseRunStepped=_PulseRunStepped,_PulseRunCharAdded=_PulseRunCharAdded,_PulseRunCharRemoving=_PulseRunCharRemoving,_PulseRunInputBegan=_PulseRunInputBegan,_PulseRunInputEnded=_PulseRunInputEnded}"
2996
3011
  );
2997
3012
  const bundleContent = Buffer.from(bundleParts.join("\n") + "\n", "utf8");
2998
3013
  const uploads = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-rb",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "rb CLI — Pulse framework build tool for Roblox script projects",
5
5
  "bin": {
6
6
  "rb": "./bin/rb.js"
@@ -73,7 +73,12 @@ do
73
73
  if #_buf > _bufMax then table.remove(_buf, 1) end
74
74
 
75
75
  if _console then
76
- if n >= 3 then warn(line) else print(line) end
76
+ -- Defer print outside the caller's thread (e.g. Heartbeat) so the
77
+ -- output always reaches the developer console regardless of context.
78
+ local _ln, _nw = line, n >= 3
79
+ task.spawn(function()
80
+ if _nw then warn(_ln) else print(_ln) end
81
+ end)
77
82
  end
78
83
  for _, fn in ipairs(_subs) do pcall(fn, entry) end
79
84
  if _file then
package/pulse/runtime.lua CHANGED
@@ -418,43 +418,106 @@ local function _needComp(name)
418
418
  return _currentComponent
419
419
  end
420
420
 
421
- local function _uid() return tostring(math.random(100000,999999)) end
421
+ -- ── Sandboxing-safe event runner ─────────────────────────────────────────────
422
+ -- Executor loadstring() environments sandbox Roblox event :Connect() — calls
423
+ -- from inside the bundle never fire. Fix: store callbacks in plain Lua tables
424
+ -- here; the compiler emits user-scope Roblox connections that call _PulseRun*,
425
+ -- routing events into these runners from the non-sandboxed user script.
426
+
427
+ local _cbSeq = 0
428
+ local function _nextId() _cbSeq = _cbSeq + 1; return _cbSeq end
429
+
430
+ local _HbCbs = {} -- Heartbeat
431
+ local _RsCbs = {} -- RenderStepped
432
+ local _StCbs = {} -- Stepped
433
+ local _CaCbs = {} -- CharacterAdded / respawn
434
+ local _CrCbs = {} -- CharacterRemoving
435
+ local _IbCbs = {} -- InputBegan
436
+ local _IeCbs = {} -- InputEnded
437
+
438
+ -- Mock RBXScriptConnection: removing from the list on :Disconnect().
439
+ local function _mockConn(id, list)
440
+ return { Disconnect = function() list[id] = nil end }
441
+ end
442
+
443
+ local function _runFrameCbs(list, a, b)
444
+ for _, e in pairs(list) do
445
+ if e and e.comp then
446
+ e.comp._rawHb = (e.comp._rawHb or 0) + 1
447
+ local pass = not e.when or e.when()
448
+ if pass and e.every then
449
+ local t = type(e.every) == "table" and e.every() or e.every
450
+ local now = tick()
451
+ if (now - e.last) < t then pass = false else e.last = now end
452
+ end
453
+ if pass then
454
+ e.comp._condHb = (e.comp._condHb or 0) + 1
455
+ if Pulse and Pulse.Log then
456
+ Pulse.Log.throttle(e.comp._name, 5, "debug", "tick (condition passed)")
457
+ end
458
+ local ok, err = pcall(e.cb, a, b)
459
+ if not ok then
460
+ warn("[Pulse] "..tostring(e.comp._name).." error: "..tostring(err))
461
+ if Pulse and Pulse.Log then
462
+ Pulse.Log.error(e.comp._name, "event error: "..tostring(err))
463
+ end
464
+ end
465
+ end
466
+ end
467
+ end
468
+ end
469
+
470
+ local function _runEventCbs(list, a, b)
471
+ for _, e in pairs(list) do
472
+ if e then pcall(e.cb, a, b) end
473
+ end
474
+ end
422
475
 
423
- local function _hbBind(svc, optsOrFn, fn)
476
+ -- Public runners called by compiler-generated user-scope connections.
477
+ _PulseRunHeartbeat = function(a,b) _runFrameCbs(_HbCbs, a,b) end
478
+ _PulseRunRenderStepped = function(a,b) _runFrameCbs(_RsCbs, a,b) end
479
+ _PulseRunStepped = function(a,b) _runFrameCbs(_StCbs, a,b) end
480
+ _PulseRunCharAdded = function(c) _runEventCbs(_CaCbs, c) end
481
+ _PulseRunCharRemoving = function(c) _runEventCbs(_CrCbs, c) end
482
+ _PulseRunInputBegan = function(i,g) _runEventCbs(_IbCbs, i,g) end
483
+ _PulseRunInputEnded = function(i,g) _runEventCbs(_IeCbs, i,g) end
484
+
485
+ local function _hbBind(list, optsOrFn, fn)
424
486
  local comp = _needComp("event")
425
487
  local opts, cb
426
488
  if type(optsOrFn) == "function" then cb=optsOrFn; opts={}
427
489
  else opts=optsOrFn; cb=fn end
428
- local when=opts.when; local every=opts.every; local last=0
429
- comp:bind("ev_".._uid(), svc:Connect(function(a,b)
430
- if when and not when() then return end
431
- if every then
432
- local t=type(every)=="table" and every() or every
433
- local now=tick(); if (now-last)<t then return end; last=now
434
- end
435
- -- Throttled trace: confirms heartbeat is running when condition passes.
436
- if Pulse and Pulse.Log then
437
- Pulse.Log.throttle(comp._name, 5, "debug", "tick (condition passed)")
438
- end
439
- local ok,err=pcall(cb,a,b)
440
- if not ok then
441
- warn("[Pulse] "..comp._name.." event error: "..tostring(err))
442
- if Pulse and Pulse.Log then
443
- Pulse.Log.error(comp._name, "tick error: "..tostring(err))
444
- end
445
- end
446
- end))
490
+ local id = _nextId()
491
+ comp._rawHb = comp._rawHb or 0
492
+ comp._condHb = comp._condHb or 0
493
+ list[id] = { comp=comp, when=opts.when, every=opts.every, last=0, cb=cb }
494
+ comp:bind("ev_"..id, _mockConn(id, list))
447
495
  end
448
496
 
449
497
  local _on = {}
450
- _on.heartbeat = function(a,b) _hbBind(_PulseRS.Heartbeat, a,b) end
451
- _on.renderStepped = function(a,b) _hbBind(_PulseRS.RenderStepped, a,b) end
452
- _on.stepped = function(a,b) _hbBind(_PulseRS.Stepped, a,b) end
453
- _on.inputBegan = function(fn) local c=_needComp("inputBegan"); c:bind("ib_".._uid(), _PulseUIS.InputBegan:Connect(fn)) end
454
- _on.inputEnded = function(fn) local c=_needComp("inputEnded"); c:bind("ie_".._uid(), _PulseUIS.InputEnded:Connect(fn)) end
455
- _on.characterAdded = function(fn) local c=_needComp("characterAdded"); c:bind("ca_".._uid(), _LocalPlayer.CharacterAdded:Connect(fn)) end
456
- _on.characterRemoving = function(fn) local c=_needComp("characterRemoving"); c:bind("cr_".._uid(), _LocalPlayer.CharacterRemoving:Connect(fn)) end
457
- _on.respawn = function(fn) _needComp("respawn"):onRespawn(fn) end
498
+ _on.heartbeat = function(a,b) _hbBind(_HbCbs, a,b) end
499
+ _on.renderStepped = function(a,b) _hbBind(_RsCbs, a,b) end
500
+ _on.stepped = function(a,b) _hbBind(_StCbs, a,b) end
501
+ _on.inputBegan = function(fn)
502
+ local c=_needComp("inputBegan"); local id=_nextId()
503
+ _IbCbs[id]={cb=fn}; c:bind("ib_"..id, _mockConn(id,_IbCbs))
504
+ end
505
+ _on.inputEnded = function(fn)
506
+ local c=_needComp("inputEnded"); local id=_nextId()
507
+ _IeCbs[id]={cb=fn}; c:bind("ie_"..id, _mockConn(id,_IeCbs))
508
+ end
509
+ _on.characterAdded = function(fn)
510
+ local c=_needComp("characterAdded"); local id=_nextId()
511
+ _CaCbs[id]={cb=fn}; c:bind("ca_"..id, _mockConn(id,_CaCbs))
512
+ end
513
+ _on.characterRemoving = function(fn)
514
+ local c=_needComp("characterRemoving"); local id=_nextId()
515
+ _CrCbs[id]={cb=fn}; c:bind("cr_"..id, _mockConn(id,_CrCbs))
516
+ end
517
+ _on.respawn = function(fn)
518
+ local c=_needComp("respawn"); local id=_nextId()
519
+ _CaCbs[id]={cb=fn}; c:bind("__respawn_"..c._name, _mockConn(id,_CaCbs))
520
+ end
458
521
  _on.signal = function(sig,fn)
459
522
  local comp = _needComp("signal")
460
523
  comp:watch(sig, function(v)
@@ -464,7 +527,7 @@ _on.signal = function(sig,fn)
464
527
  fn(v)
465
528
  end)
466
529
  end
467
- _on.after = function(s,fn) _needComp("after"):task(s,fn) end
530
+ _on.after = function(s,fn) _needComp("after"):task(s,fn) end
468
531
 
469
532
  local function _defineComponent(name, setup)
470
533
  local comp = Component(name)
@@ -490,6 +553,7 @@ local function _groupbox(side, title, opts)
490
553
  return { type="groupbox", side=side, title=title,
491
554
  icon=(opts and opts.icon) or "",
492
555
  mount=(opts and opts.mount) or nil,
556
+ plain=(opts and opts.plain) or false,
493
557
  widgets=(opts and opts.widgets) or {} }
494
558
  end
495
559