pulse-rb 1.4.0 → 1.4.2

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.
@@ -2,23 +2,28 @@
2
2
  -- Wind UI adapter for Pulse components.
3
3
  -- Docs: footagesus.github.io/treehub-web/docs/windui
4
4
  -- Version: 1.6.64-fix (pinned)
5
-
6
- -- Executor loadstring() environments are sandboxed — each chunk has its OWN environment
7
- -- table, separate from _G. Global reads in this chunk go through getfenv(1), not _G.
8
- -- The compiler passes (_P, _PULSE_LAYOUT) as arguments.
9
- local _P, _PULSE_LAYOUT = ...
10
- if type(_P) == "table" then
5
+ --
6
+ -- Supports two load modes:
7
+ -- 1. Combined (rb publish): inlined into pulse.lua _PULSE_LAYOUT, Components, _Pages, Pulse
8
+ -- are already in scope as upvalues. publish.ts strips everything above [PULSE_INLINE_START].
9
+ -- 2. Separate (legacy): loaded via its own loadstring(_P, _PULSE_LAYOUT) call.
10
+ -- _P carries the bundle table; we spread it into the local env.
11
+ local _isSeparate = type(select(1,...)) == "table"
12
+ if _isSeparate then
13
+ local _P = select(1,...)
14
+ local _PULSE_LAYOUT = select(2,...) or {}
11
15
  local _env = (getfenv and getfenv(1)) or _ENV or _G
12
16
  for _k, _v in pairs(_P) do _env[_k] = _v end
13
17
  end
14
18
  _PULSE_LAYOUT = _PULSE_LAYOUT or {}
15
19
 
16
- -- Capture bundle table references as upvalues immediately so the startup block
17
- -- can access them without global lookups (which are environment-table-dependent
18
- -- in executor sandboxes and may fail in task.spawn callbacks).
19
- local _BundleComponents = type(_P) == "table" and _P.Components or {}
20
- local _BundlePages = type(_P) == "table" and _P._Pages or {}
21
- local _BundlePulse = type(_P) == "table" and _P.Pulse or Pulse
20
+ -- [PULSE_INLINE_START]
21
+ -- Everything below this marker is included verbatim when bundled inline.
22
+ -- In combined mode: Components, _Pages, Pulse are direct upvalues from runtime.lua.
23
+ -- In separate mode: they were spread into the local env by the block above.
24
+ local _BundleComponents = Components or {}
25
+ local _BundlePages = _Pages or {}
26
+ local _BundlePulse = Pulse or {}
22
27
 
23
28
  local _WINDUI_URL = "https://github.com/Footagesus/WindUI/releases/download/1.6.64-fix/main.lua"
24
29
  local _PULSE_LOGO = "https://pulse-rb.vercel.app/img/logo.svg"
@@ -184,6 +189,9 @@ function _UIAdapter:CreateWindow(title, w, h, opts)
184
189
  local tabProxy = { _wind = windTab }
185
190
  function tabProxy:AddLeftGroupbox(title, gbIcon) return _mkSection(leftCol, title, gbIcon) end
186
191
  function tabProxy:AddRightGroupbox(title, gbIcon) return _mkSection(rightCol, title, gbIcon) end
192
+ -- Raw column access — no Section wrapper (used by plain=true groupboxes)
193
+ function tabProxy:GetLeftColumn() return { _container = leftCol } end
194
+ function tabProxy:GetRightColumn() return { _container = rightCol } end
187
195
  return tabProxy
188
196
  end
189
197
 
@@ -587,14 +595,25 @@ task.spawn(function()
587
595
  local _execVer = ""
588
596
  pcall(function()
589
597
  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"))
598
+ local ok, n, v = pcall(identifyexecutor)
599
+ if ok then _execName = tostring(n or "Unknown"); _execVer = tostring(v or "") end
600
+ end
601
+ if _execName == "Unknown" and type(rawget(_G, "getexecutorname")) == "function" then
602
+ local ok, n = pcall(getexecutorname)
603
+ if ok and n then _execName = tostring(n) end
604
+ end
605
+ if _execName == "Unknown" then
606
+ if rawget(_G, "syn") or rawget(_G, "Synapse") then _execName = "Synapse X"
607
+ elseif rawget(_G, "KRNL_LOADED") then _execName = "Krnl"
608
+ elseif rawget(_G, "DELTA_KEY") then _execName = "Delta"
609
+ elseif rawget(_G, "WAVE_LOADED") then _execName = "Wave"
610
+ elseif rawget(_G, "SOLARA_LOADED") then _execName = "Solara"
611
+ elseif rawget(_G, "XENO_LOADED") then _execName = "Xeno"
612
+ elseif rawget(_G, "Seliware") or rawget(_G, "seliware") then _execName = "Seliware"
613
+ elseif rawget(_G, "Celery") or rawget(_G, "celery") then _execName = "Celery"
614
+ elseif rawget(_G, "is_sirhurt_closure") then _execName = "Sirhurt"
615
+ elseif rawget(_G, "EXECUTOR_NAME") then _execName = tostring(rawget(_G, "EXECUTOR_NAME"))
616
+ end
598
617
  end
599
618
  end)
600
619
 
@@ -605,84 +624,131 @@ task.spawn(function()
605
624
  if info and info.Name then _gameName = info.Name end
606
625
  end)
607
626
 
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
627
+ -- Home tab — raw columns, no Section wrappers for a clean look.
628
+ -- Create the tab directly so we can get the raw VStack columns from _mkColumns.
629
+ local _homeWindTab
630
+ pcall(function() _homeWindTab = _windWindow:Tab({ Title = "Home", Icon = "house" }) end)
631
+ if not _UIAdapter._firstTab then _UIAdapter._firstTab = _homeWindTab end
632
+ local _homeLeft, _homeRight = _mkColumns(_homeWindTab)
633
+
634
+ -- Left: player info + description + discord (no Section box)
635
+ if _homeLeft then
636
+ -- Capture the returned paragraph element so we can try avatar injection via its Frame
637
+ local _paraRef = nil
616
638
  pcall(function()
617
- _welcomeGb._container:Image({
618
- Source = _avatarUrl,
619
- Size = UDim2.fromOffset(96, 96),
639
+ _paraRef = _homeLeft:Paragraph({
640
+ Title = "Welcome, " .. _dspName,
641
+ Desc = "@" .. _usrName,
620
642
  })
621
643
  end)
622
- end
623
- _welcomeGb._container:Paragraph({
624
- Title = _dspName,
625
- Desc = "@" .. _usrName,
626
- })
627
644
 
628
- if L.description and L.description ~= "" then
629
- _welcomeGb._container:Divider()
630
- _welcomeGb._container:Paragraph({ Title = "", Desc = L.description })
631
- end
645
+ -- Avatar injection: WindUI paragraph elements wrap a Roblox Frame internally.
646
+ -- Grab that Frame's parent (the list container) and add an ImageLabel there.
647
+ if _avatarUrl ~= "" and _paraRef then
648
+ task.delay(0.3, function()
649
+ pcall(function()
650
+ local paraFrame = nil
651
+ for _, k in ipairs({"Frame","Object","Instance","Container","Label","Content"}) do
652
+ local ok, v = pcall(function() return _paraRef[k] end)
653
+ if ok and type(v) == "userdata" then
654
+ local _, isGui = pcall(function() return v:IsA("GuiObject") end)
655
+ if isGui then paraFrame = v; break end
656
+ end
657
+ end
658
+ local contentFrame = paraFrame and paraFrame.Parent
659
+ if not (contentFrame and type(contentFrame) == "userdata") then return end
660
+ if contentFrame:FindFirstChild("_PulseAvatar") then return end
661
+ local img = Instance.new("ImageLabel")
662
+ img.Name = "_PulseAvatar"; img.Image = _avatarUrl
663
+ img.Size = UDim2.fromOffset(64, 64); img.BackgroundTransparency = 1
664
+ img.ScaleType = Enum.ScaleType.Crop; img.LayoutOrder = -999
665
+ Instance.new("UICorner", img).CornerRadius = UDim.new(1, 0)
666
+ img.Parent = contentFrame
667
+ end)
668
+ end)
669
+ end
632
670
 
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
- })
671
+ if L.description and L.description ~= "" then
672
+ pcall(function() _homeLeft:Divider() end)
673
+ pcall(function() _homeLeft:Paragraph({ Title = "About", Desc = L.description }) end)
674
+ end
675
+ if L.discord and L.discord ~= "" then
676
+ pcall(function() _homeLeft:Divider() end)
677
+ pcall(function()
678
+ _homeLeft:Button({
679
+ Title = "Join Discord",
680
+ Desc = "Tap to copy the invite link",
681
+ Callback = function()
682
+ local ok = false
683
+ if not ok then pcall(function() setclipboard(L.discord); ok=true end) end
684
+ if not ok then pcall(function() toclipboard(L.discord); ok=true end) end
685
+ if not ok then pcall(function() writeclipboard(L.discord); ok=true end) end
686
+ _PulseNotify(ok and "Discord link copied!" or "No clipboard API", 3)
687
+ end,
688
+ })
689
+ end)
690
+ end
646
691
  end
647
692
 
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, ", "),
693
+ -- Right: script info + executor + game (no Section box)
694
+ if _homeRight then
695
+ local _verStr = (L.version and L.version ~= "") and ("v" .. L.version) or ""
696
+ local _authStr = (L.author and L.author ~= "") and ("by " .. L.author) or ""
697
+ local _metaParts = {}
698
+ if _verStr ~= "" then _metaParts[#_metaParts+1] = _verStr end
699
+ if _authStr ~= "" then _metaParts[#_metaParts+1] = _authStr end
700
+ pcall(function()
701
+ _homeRight:Paragraph({
702
+ Title = L.title or "Hub",
703
+ Desc = #_metaParts > 0 and table.concat(_metaParts, " · ") or "",
704
+ })
705
+ end)
706
+ pcall(function() _homeRight:Divider() end)
707
+ local _execStr = _execName .. (_execVer ~= "" and (" " .. _execVer) or "")
708
+ pcall(function() _homeRight:Paragraph({ Title = "Executor", Desc = _execStr }) end)
709
+ pcall(function() _homeRight:Paragraph({ Title = "Game", Desc = _gameName }) end)
710
+ pcall(function() _homeRight:Divider() end)
711
+ -- Server info: live player count + rejoin
712
+ local _playerCount = 0
713
+ pcall(function() _playerCount = #game:GetService("Players"):GetPlayers() end)
714
+ pcall(function()
715
+ _homeRight:Paragraph({
716
+ Title = "Server",
717
+ Desc = tostring(_playerCount) .. " player" .. (_playerCount == 1 and "" or "s") .. " · " .. tostring(game.PlaceId),
685
718
  })
719
+ end)
720
+ pcall(function()
721
+ _homeRight:Button({
722
+ Title = "Rejoin Server",
723
+ Desc = "Leave and rejoin the current place",
724
+ Callback = function()
725
+ pcall(function()
726
+ game:GetService("TeleportService"):TeleportToPlaceInstance(
727
+ game.PlaceId, game.JobId,
728
+ game:GetService("Players").LocalPlayer
729
+ )
730
+ end)
731
+ end,
732
+ })
733
+ end)
734
+ if L.unsupported and L.unsupported ~= "" then
735
+ local _unsupList = {}
736
+ if type(L.unsupported) == "table" then
737
+ for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
738
+ elseif type(L.unsupported) == "string" then
739
+ for part in L.unsupported:gmatch("[^,]+") do
740
+ _unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
741
+ end
742
+ end
743
+ if #_unsupList > 0 then
744
+ pcall(function() _homeRight:Divider() end)
745
+ pcall(function()
746
+ _homeRight:Paragraph({
747
+ Title = "Not compatible with:",
748
+ Desc = table.concat(_unsupList, ", "),
749
+ })
750
+ end)
751
+ end
686
752
  end
687
753
  end
688
754
 
@@ -691,7 +757,14 @@ task.spawn(function()
691
757
  local tab = _win:AddTab(page.title, page.icon)
692
758
  for _, gb in ipairs(page.layout or {}) do
693
759
  local container
694
- if gb.side == "left" then
760
+ if gb.plain then
761
+ -- No Section wrapper — widgets go directly onto the column VStack
762
+ if gb.side == "left" then
763
+ if tab.GetLeftColumn then container = tab:GetLeftColumn() end
764
+ else
765
+ if tab.GetRightColumn then container = tab:GetRightColumn() end
766
+ end
767
+ elseif gb.side == "left" then
695
768
  container = tab:AddLeftGroupbox(gb.title, gb.icon)
696
769
  else
697
770
  container = tab:AddRightGroupbox(gb.title, gb.icon)
@@ -784,6 +857,19 @@ task.spawn(function()
784
857
  key = _PULSE_TOGGLE_KEY,
785
858
  action = function() _UIAdapter:ToggleWindow() end,
786
859
  })
860
+ gb_menu._container:Button({
861
+ Title = "Rejoin",
862
+ Icon = "refresh-cw",
863
+ Desc = "Leave and rejoin the current place",
864
+ Callback = function()
865
+ pcall(function()
866
+ game:GetService("TeleportService"):TeleportToPlaceInstance(
867
+ game.PlaceId, game.JobId,
868
+ game:GetService("Players").LocalPlayer
869
+ )
870
+ end)
871
+ end,
872
+ })
787
873
 
788
874
  -- Auto-load saved config on startup
789
875
  task.spawn(function()
@@ -799,14 +885,19 @@ task.spawn(function()
799
885
  local _devTab
800
886
  pcall(function() _devTab = _windWindow:Tab({ Title = "Dev", Icon = "bug" }) end)
801
887
  if _devTab then
802
- local _devCol
803
- pcall(function() _devCol = _devTab:VStack({}) end)
804
- _devCol = _devCol or _devTab
888
+ local _devLeft, _devRight
889
+ pcall(function()
890
+ local _devHs = _devTab:HStack({ AutoSpace = true })
891
+ _devLeft = _devHs:VStack({})
892
+ _devRight = _devHs:VStack({})
893
+ end)
894
+ _devLeft = _devLeft or _devTab
895
+ _devRight = _devRight or _devTab
805
896
 
806
- -- Tools section
897
+ -- Tools section (left)
807
898
  local _toolSect
808
899
  pcall(function()
809
- _toolSect = _devCol:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
900
+ _toolSect = _devLeft:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
810
901
  end)
811
902
 
812
903
  local _TOOLS = {
@@ -864,7 +955,7 @@ task.spawn(function()
864
955
  -- Scanner section
865
956
  local _scanSect
866
957
  pcall(function()
867
- _scanSect = _devCol:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
958
+ _scanSect = _devRight:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
868
959
  end)
869
960
 
870
961
  if _scanSect then
@@ -976,7 +1067,7 @@ task.spawn(function()
976
1067
  -- Info section — framework version + signal counts for debugging
977
1068
  local _infoSect
978
1069
  pcall(function()
979
- _infoSect = _devCol:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
1070
+ _infoSect = _devLeft:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
980
1071
  end)
981
1072
  if _infoSect then
982
1073
  local nPages = #_BundlePages
@@ -1000,6 +1091,47 @@ task.spawn(function()
1000
1091
  end
1001
1092
  end
1002
1093
 
1094
+ -- Heartbeat Monitor section — tick counts per component (left)
1095
+ -- Uses Pulse.Monitor which is independent of the log system.
1096
+ local _monSect
1097
+ pcall(function()
1098
+ _monSect = _devLeft:Section({ Title="Heartbeat Monitor", Icon="activity", Box=true, BoxBorder=true, Opened=true })
1099
+ end)
1100
+ if _monSect then
1101
+ local _monPara
1102
+ pcall(function()
1103
+ _monPara = _monSect:Paragraph({
1104
+ Title = "raw / cond (updates 2s)",
1105
+ Desc = "raw = every HB fire · cond = when() passed",
1106
+ })
1107
+ end)
1108
+ -- Reads _rawHb/_condHb plain-Lua counters set in runtime._hbBind.
1109
+ -- No dependency on Pulse.Monitor — works regardless of log system state.
1110
+ task.spawn(function()
1111
+ while _windWindow do
1112
+ task.wait(2)
1113
+ pcall(function()
1114
+ local lines = {}
1115
+ for cname, comp in pairs(_BundleComponents) do
1116
+ if type(comp) == "table" and comp._rawHb ~= nil then
1117
+ lines[#lines+1] = cname
1118
+ .. " raw=" .. tostring(comp._rawHb)
1119
+ .. " cond=" .. tostring(comp._condHb)
1120
+ end
1121
+ end
1122
+ if _monPara then
1123
+ if #lines == 0 then
1124
+ _monPara:SetDesc("No heartbeat components registered")
1125
+ else
1126
+ table.sort(lines)
1127
+ _monPara:SetDesc(table.concat(lines, "\n"))
1128
+ end
1129
+ end
1130
+ end)
1131
+ end
1132
+ end)
1133
+ end
1134
+
1003
1135
  -- Console section — controls for the Roblox developer console (F9).
1004
1136
  -- "Clear" wipes the output so only your next prints are visible.
1005
1137
  -- "Print Logs" re-dumps all buffered Pulse.Log entries so you can read
@@ -1007,7 +1139,7 @@ task.spawn(function()
1007
1139
  -- "Copy Logs" copies the full log buffer to clipboard.
1008
1140
  local _consoleSect
1009
1141
  pcall(function()
1010
- _consoleSect = _devCol:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
1142
+ _consoleSect = _devRight:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
1011
1143
  end)
1012
1144
  if _consoleSect and _BundlePulse and _BundlePulse.Log then
1013
1145
  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.2";
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");
@@ -1363,7 +1363,7 @@ end)
1363
1363
  if (opts.dev) parts.push(`,dev=true`);
1364
1364
  parts.push(`}
1365
1365
  `);
1366
- parts.push(`local _P=loadstring(game:HttpGet("${base}/bundle.lua"))()
1366
+ parts.push(`local _P=loadstring(game:HttpGet("${base}/pulse.lua"))(_PULSE_LAYOUT)
1367
1367
  `);
1368
1368
  if (opts.dev) {
1369
1369
  parts.push(`_PULSE_DEV = true
@@ -1373,8 +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(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))(_P,_PULSE_LAYOUT)
1377
- `);
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
+ );
1378
1391
  parts.push(`local signal,computed,defineComponent,on=_P.signal,_P.computed,_P.defineComponent,_P.on
1379
1392
  `);
1380
1393
  parts.push(`local toggle,slider,dropdown,multidropdown=_P.toggle,_P.slider,_P.dropdown,_P.multidropdown
@@ -2982,7 +2995,9 @@ async function cmdPublish(_args) {
2982
2995
  process.exit(1);
2983
2996
  }
2984
2997
  const bundleParts = [
2985
- `-- Pulse v${RB_VERSION} bundle (runtime + helpers)`,
2998
+ `-- Pulse v${RB_VERSION} (runtime + helpers + UI adapter)`,
2999
+ `-- Single loadstring \u2014 no executor sandbox split between runtime and UI.`,
3000
+ `local _PULSE_LAYOUT = ... or {}`,
2986
3001
  fs.readFileSync(runtimeFile, "utf8").trimEnd()
2987
3002
  ];
2988
3003
  if (fs.existsSync(helpersDir)) {
@@ -2991,21 +3006,22 @@ async function cmdPublish(_args) {
2991
3006
  bundleParts.push(fs.readFileSync(pathe.join(helpersDir, f), "utf8").trimEnd());
2992
3007
  }
2993
3008
  }
3009
+ const winduiPath = pathe.join(ADAPTERS, "windui.lua");
3010
+ if (fs.existsSync(winduiPath)) {
3011
+ const winduiSrc = fs.readFileSync(winduiPath, "utf8");
3012
+ const markerTag = "-- [PULSE_INLINE_START]";
3013
+ const markerIdx = winduiSrc.indexOf(markerTag);
3014
+ const winduiCore = markerIdx >= 0 ? winduiSrc.slice(markerIdx + markerTag.length).trimStart() : winduiSrc;
3015
+ bundleParts.push("-- [windui adapter inline]");
3016
+ bundleParts.push(winduiCore.trimEnd());
3017
+ }
2994
3018
  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}"
3019
+ "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
3020
  );
2997
3021
  const bundleContent = Buffer.from(bundleParts.join("\n") + "\n", "utf8");
2998
3022
  const uploads = [
2999
- { remote: `${VERSION_PATH}/bundle.lua`, content: bundleContent }
3023
+ { remote: `${VERSION_PATH}/pulse.lua`, content: bundleContent }
3000
3024
  ];
3001
- for (const adapter of ["windui"]) {
3002
- const p = pathe.join(ADAPTERS, `${adapter}.lua`);
3003
- if (!fs.existsSync(p)) continue;
3004
- uploads.push({
3005
- remote: `${VERSION_PATH}/adapters/${adapter}.lua`,
3006
- content: Buffer.from(fs.readFileSync(p, "utf8"), "utf8")
3007
- });
3008
- }
3009
3025
  pSection(`Uploading to R2 ${gray("(v" + RB_VERSION + " \xB7 " + uploads.length + " files)")}`);
3010
3026
  for (const { remote, content } of uploads) {
3011
3027
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-rb",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
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
@@ -186,17 +186,6 @@ local function Component(name)
186
186
  end)
187
187
  end
188
188
 
189
- -- Respawn listener (bound to connection tracker)
190
- function c:onRespawn(fn)
191
- local key = "__respawn_" .. name
192
- local existing = self._connections[key]
193
- if existing then
194
- pcall(function() existing:Disconnect() end)
195
- end
196
- self._connections[key] = game:GetService("Players").LocalPlayer
197
- .CharacterAdded:Connect(fn)
198
- end
199
-
200
189
  -- Lifecycle
201
190
  function c:mount()
202
191
  if self._mounted then return end
@@ -418,43 +407,106 @@ local function _needComp(name)
418
407
  return _currentComponent
419
408
  end
420
409
 
421
- local function _uid() return tostring(math.random(100000,999999)) end
410
+ -- ── Sandboxing-safe event runner ─────────────────────────────────────────────
411
+ -- Executor loadstring() environments sandbox Roblox event :Connect() — calls
412
+ -- from inside the bundle never fire. Fix: store callbacks in plain Lua tables
413
+ -- here; the compiler emits user-scope Roblox connections that call _PulseRun*,
414
+ -- routing events into these runners from the non-sandboxed user script.
415
+
416
+ local _cbSeq = 0
417
+ local function _nextId() _cbSeq = _cbSeq + 1; return _cbSeq end
418
+
419
+ local _HbCbs = {} -- Heartbeat
420
+ local _RsCbs = {} -- RenderStepped
421
+ local _StCbs = {} -- Stepped
422
+ local _CaCbs = {} -- CharacterAdded / respawn
423
+ local _CrCbs = {} -- CharacterRemoving
424
+ local _IbCbs = {} -- InputBegan
425
+ local _IeCbs = {} -- InputEnded
426
+
427
+ -- Mock RBXScriptConnection: removing from the list on :Disconnect().
428
+ local function _mockConn(id, list)
429
+ return { Disconnect = function() list[id] = nil end }
430
+ end
431
+
432
+ local function _runFrameCbs(list, a, b)
433
+ for _, e in pairs(list) do
434
+ if e and e.comp then
435
+ e.comp._rawHb = (e.comp._rawHb or 0) + 1
436
+ local pass = not e.when or e.when()
437
+ if pass and e.every then
438
+ local t = type(e.every) == "table" and e.every() or e.every
439
+ local now = tick()
440
+ if (now - e.last) < t then pass = false else e.last = now end
441
+ end
442
+ if pass then
443
+ e.comp._condHb = (e.comp._condHb or 0) + 1
444
+ if Pulse and Pulse.Log then
445
+ Pulse.Log.throttle(e.comp._name, 5, "debug", "tick (condition passed)")
446
+ end
447
+ local ok, err = pcall(e.cb, a, b)
448
+ if not ok then
449
+ warn("[Pulse] "..tostring(e.comp._name).." error: "..tostring(err))
450
+ if Pulse and Pulse.Log then
451
+ Pulse.Log.error(e.comp._name, "event error: "..tostring(err))
452
+ end
453
+ end
454
+ end
455
+ end
456
+ end
457
+ end
458
+
459
+ local function _runEventCbs(list, a, b)
460
+ for _, e in pairs(list) do
461
+ if e then pcall(e.cb, a, b) end
462
+ end
463
+ end
422
464
 
423
- local function _hbBind(svc, optsOrFn, fn)
465
+ -- Public runners called by compiler-generated user-scope connections.
466
+ _PulseRunHeartbeat = function(a,b) _runFrameCbs(_HbCbs, a,b) end
467
+ _PulseRunRenderStepped = function(a,b) _runFrameCbs(_RsCbs, a,b) end
468
+ _PulseRunStepped = function(a,b) _runFrameCbs(_StCbs, a,b) end
469
+ _PulseRunCharAdded = function(c) _runEventCbs(_CaCbs, c) end
470
+ _PulseRunCharRemoving = function(c) _runEventCbs(_CrCbs, c) end
471
+ _PulseRunInputBegan = function(i,g) _runEventCbs(_IbCbs, i,g) end
472
+ _PulseRunInputEnded = function(i,g) _runEventCbs(_IeCbs, i,g) end
473
+
474
+ local function _hbBind(list, optsOrFn, fn)
424
475
  local comp = _needComp("event")
425
476
  local opts, cb
426
477
  if type(optsOrFn) == "function" then cb=optsOrFn; opts={}
427
478
  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))
479
+ local id = _nextId()
480
+ comp._rawHb = comp._rawHb or 0
481
+ comp._condHb = comp._condHb or 0
482
+ list[id] = { comp=comp, when=opts.when, every=opts.every, last=0, cb=cb }
483
+ comp:bind("ev_"..id, _mockConn(id, list))
447
484
  end
448
485
 
449
486
  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
487
+ _on.heartbeat = function(a,b) _hbBind(_HbCbs, a,b) end
488
+ _on.renderStepped = function(a,b) _hbBind(_RsCbs, a,b) end
489
+ _on.stepped = function(a,b) _hbBind(_StCbs, a,b) end
490
+ _on.inputBegan = function(fn)
491
+ local c=_needComp("inputBegan"); local id=_nextId()
492
+ _IbCbs[id]={cb=fn}; c:bind("ib_"..id, _mockConn(id,_IbCbs))
493
+ end
494
+ _on.inputEnded = function(fn)
495
+ local c=_needComp("inputEnded"); local id=_nextId()
496
+ _IeCbs[id]={cb=fn}; c:bind("ie_"..id, _mockConn(id,_IeCbs))
497
+ end
498
+ _on.characterAdded = function(fn)
499
+ local c=_needComp("characterAdded"); local id=_nextId()
500
+ _CaCbs[id]={cb=fn}; c:bind("ca_"..id, _mockConn(id,_CaCbs))
501
+ end
502
+ _on.characterRemoving = function(fn)
503
+ local c=_needComp("characterRemoving"); local id=_nextId()
504
+ _CrCbs[id]={cb=fn}; c:bind("cr_"..id, _mockConn(id,_CrCbs))
505
+ end
506
+ _on.respawn = function(fn)
507
+ local c=_needComp("respawn"); local id=_nextId()
508
+ _CaCbs[id]={cb=fn}; c:bind("__respawn_"..c._name, _mockConn(id,_CaCbs))
509
+ end
458
510
  _on.signal = function(sig,fn)
459
511
  local comp = _needComp("signal")
460
512
  comp:watch(sig, function(v)
@@ -464,7 +516,7 @@ _on.signal = function(sig,fn)
464
516
  fn(v)
465
517
  end)
466
518
  end
467
- _on.after = function(s,fn) _needComp("after"):task(s,fn) end
519
+ _on.after = function(s,fn) _needComp("after"):task(s,fn) end
468
520
 
469
521
  local function _defineComponent(name, setup)
470
522
  local comp = Component(name)
@@ -490,6 +542,7 @@ local function _groupbox(side, title, opts)
490
542
  return { type="groupbox", side=side, title=title,
491
543
  icon=(opts and opts.icon) or "",
492
544
  mount=(opts and opts.mount) or nil,
545
+ plain=(opts and opts.plain) or false,
493
546
  widgets=(opts and opts.widgets) or {} }
494
547
  end
495
548