--!strict --[[ Helios for Roblox Studio — autonomous edition. =============================================== Drop this file into your Studio Plugins folder: Windows : %LOCALAPPDATA%\Roblox\Plugins\ macOS : ~/Documents/Roblox/Plugins/ Reload Studio. A "Helios" button appears in the Plugins tab. First-run: click API in the plugin header, paste a key from https://nyptid.com/keys. AUTONOMOUS MODE --------------- Helios can EXECUTE changes in your place — create scripts, edit scripts, create RemoteEvents / Folders / Parts, set properties, delete Instances. Every applied change goes through ChangeHistoryService so Ctrl+Z reverts cleanly. Safety: • Default: each action shows up with an APPLY button. Nothing runs until you click it. • Autopilot toggle (top-right of dock): when ON, actions auto- apply 0.4s after they appear. Use only when you trust the session. • Every action is recorded as a single waypoint — Ctrl+Z reverts the whole batch in one stroke. Casey @ NYPTID Industries — 2026-05-06 ]] local HttpService = game:GetService("HttpService") local Selection = game:GetService("Selection") local ChangeHistoryService = game:GetService("ChangeHistoryService") -- ScriptEditorService is the right way to update script source in -- modern Studio. Falls back to writing .Source directly on older -- builds where it's missing. local ScriptEditorService: any = nil do local ok, svc = pcall(function() return game:GetService("ScriptEditorService") end) if ok then ScriptEditorService = svc end end -- ── Config ──────────────────────────────────────────────────────────── local API_BASE = "https://nyptid.com" local API_CHAT = API_BASE .. "/api/chat" local PLUGIN_NAME = "Helios" local SETTINGS_KEY_API = "helios_api_key" local SETTINGS_KEY_HISTORY = "helios_history_v1" local SETTINGS_KEY_AUTOPILOT = "helios_autopilot" local AUTOPILOT_DELAY_SEC = 0.4 -- ── Plugin shell ────────────────────────────────────────────────────── local toolbar = plugin:CreateToolbar(PLUGIN_NAME) -- Casey 2026-05-06 v1.3: removed the bogus rbxassetid that was throwing -- "Unable to load plugin icon" in Output. Empty icon = button shows "Helios" -- text only. Cleaner than a broken icon ref. local toggleButton = toolbar:CreateButton( "Helios", "Open Helios — your AI building partner", "" ) toggleButton.ClickableWhenViewportHidden = true -- Casey 2026-05-06 v1.4: Float wasn't honored on Casey's Studio because -- the third arg (InitialEnabledShouldOverrideRestore) was false, which -- told Studio to use the remembered state from previous sessions — -- where the widget was docked + crushed. Setting true forces Studio -- to use the InitialDockState (Float) on every load. -- -- Also bumped the widget ID from "HeliosDock" → "HeliosDockV14" so -- Studio creates a fresh widget rather than restoring the previous -- v1.0/v1.1/v1.2 cached state. Old "HeliosDock" entries are -- abandoned (no harm — they just don't render anything). local widgetInfo = DockWidgetPluginGuiInfo.new( Enum.InitialDockState.Float, false, true, 460, 720, 360, 460 ) local widget: DockWidgetPluginGui = plugin:CreateDockWidgetPluginGui("HeliosDockV14", widgetInfo) widget.Title = "Helios" widget.Name = "HeliosDockV14" -- ── State ───────────────────────────────────────────────────────────── type Message = { role: "user" | "assistant", content: string } local conversation: { Message } = {} local sending = false local autopilot = plugin:GetSetting(SETTINGS_KEY_AUTOPILOT) == true local function loadApiKey(): string? local saved = plugin:GetSetting(SETTINGS_KEY_API) if type(saved) == "string" and #saved > 0 then return saved end return nil end local function saveApiKey(k: string) plugin:SetSetting(SETTINGS_KEY_API, k) end local function loadHistory() local saved = plugin:GetSetting(SETTINGS_KEY_HISTORY) if type(saved) == "table" then for _, m in ipairs(saved) do if type(m) == "table" and type(m.role) == "string" and type(m.content) == "string" then table.insert(conversation, { role = m.role :: any, content = m.content }) end end end end local function saveHistory() plugin:SetSetting(SETTINGS_KEY_HISTORY, conversation) end loadHistory() -- ── Theme helpers ───────────────────────────────────────────────────── -- -- Casey 2026-05-06: HOTFIX. Was using settings().Studio.Theme:GetColor() -- with Enum.StudioStyleGuideColor[name] lookups. On some Studio builds -- one or more of those enum names don't resolve, which crashes the -- plugin silently before any UI renders (Casey's friend hit this: -- toolbar + dock title appeared but the inside was blank white). -- -- New approach: hardcoded dark-theme palette with brand cyan/amber. -- Looks the same in Studio's light or dark mode (always dark UI for -- the Helios dock — readable + on-brand). Zero dependency on Studio's -- theme API, so it can't break across Studio versions. local CYAN = Color3.fromRGB(34, 211, 238) local AMBER = Color3.fromRGB(244, 196, 48) local SUCCESS = Color3.fromRGB(34, 197, 94) local DANGER = Color3.fromRGB(239, 68, 68) local PALETTE: { [string]: Color3 } = { MainBackground = Color3.fromRGB(20, 22, 26), Titlebar = Color3.fromRGB(14, 16, 20), ScriptBackground = Color3.fromRGB(28, 30, 35), Button = Color3.fromRGB(45, 48, 55), ButtonHover = Color3.fromRGB(60, 64, 72), Border = Color3.fromRGB(65, 70, 80), MainText = Color3.fromRGB(225, 228, 232), DimmedText = Color3.fromRGB(140, 145, 155), InputFieldBackground = Color3.fromRGB(12, 14, 18), } local function color(name: string): Color3 return PALETTE[name] or Color3.fromRGB(40, 40, 40) end -- Diagnostic output — if anything goes wrong loading the plugin, we want -- the error to surface in Studio's Output window (View → Output) rather -- than fail silently. Wrap risky call sites in pcall + warn(). print("[Helios v1.5] boot start — palette resolved") -- ── Path resolution ──────────────────────────────────────────────────── -- Service shortcuts so "ServerScriptService.Foo" works without the -- redundant "game." prefix. local SERVICE_SHORTCUTS: { [string]: string } = { Workspace = "Workspace", ServerScriptService = "ServerScriptService", ServerStorage = "ServerStorage", ReplicatedStorage = "ReplicatedStorage", ReplicatedFirst = "ReplicatedFirst", StarterPlayer = "StarterPlayer", StarterGui = "StarterGui", StarterPack = "StarterPack", Lighting = "Lighting", SoundService = "SoundService", Teams = "Teams", Chat = "Chat", HttpService = "HttpService", DataStoreService = "DataStoreService", } -- Walk a dot-path under DataModel. If `create_missing_folders` is true, -- intermediate Folders are auto-created (handy for create_script). -- Returns the leaf Instance, or (nil, errMsg). local function resolvePath(path: string, create_missing_folders: boolean?): (Instance?, string?) if not path or #path == 0 then return nil, "empty path" end -- Strip leading "game." if present if path:sub(1, 5):lower() == "game." then path = path:sub(6) end local parts = {} for seg in path:gmatch("([^%.]+)") do table.insert(parts, seg) end if #parts == 0 then return nil, "empty path" end -- First segment = service or top-level child local first = parts[1] local current: Instance local ok, srv = pcall(function() return game:GetService(SERVICE_SHORTCUTS[first] or first) end) if ok and srv then current = srv else local child = game:FindFirstChild(first) if not child then return nil, "unknown root: " .. first end current = child end -- Walk remaining for i = 2, #parts do local seg = parts[i] local child = current:FindFirstChild(seg) if not child then if create_missing_folders and i < #parts then child = Instance.new("Folder") child.Name = seg child.Parent = current else return nil, "not found: " .. table.concat(parts, ".", 1, i) end end current = child end return current, nil end -- Resolve a parent path — but if the leaf doesn't exist yet, return its -- intended parent + the leaf name. Used by create_script / create_instance. -- Returns (parent_instance, leaf_name, errMsg). local function resolveParentAndLeaf(path: string, create_missing_folders: boolean?): (Instance?, string?, string?) if not path or #path == 0 then return nil, nil, "empty path" end if path:sub(1, 5):lower() == "game." then path = path:sub(6) end local parts = {} for seg in path:gmatch("([^%.]+)") do table.insert(parts, seg) end if #parts == 0 then return nil, nil, "empty path" end if #parts == 1 then -- Direct service / top-level: parent IS DataModel, leaf is the name return game, parts[1], nil end local leaf = parts[#parts] local parentPath = table.concat(parts, ".", 1, #parts - 1) local parent, err = resolvePath(parentPath, create_missing_folders) if not parent then return nil, nil, err end return parent, leaf, nil end -- ── Property setter (handles Color3 / Vector3 / NumberRange / etc) ──── local function applyProperties(inst: Instance, props: { [string]: any }): string? for name, value in pairs(props) do local ok, err = pcall(function() -- Convert array tables into native types when shape suggests it if type(value) == "table" and #value == 3 and typeof(value[1]) == "number" then -- Heuristic: 3-number array → Color3 if name suggests color, else Vector3 local n = name:lower() if n:find("color") or n:find("tint") then (inst :: any)[name] = Color3.new(value[1], value[2], value[3]) else (inst :: any)[name] = Vector3.new(value[1], value[2], value[3]) end elseif type(value) == "table" and #value == 2 and typeof(value[1]) == "number" then (inst :: any)[name] = Vector2.new(value[1], value[2]) else (inst :: any)[name] = value end end) if not ok then return string.format("property %s: %s", name, tostring(err)) end end return nil end -- ── Action executors ────────────────────────────────────────────────── type Action = { type: string, rationale: string?, path: string?, scriptType: string?, source: string?, className: string?, name: string?, parent: string?, properties: { [string]: any }?, } type ActionResult = { ok: boolean, msg: string } local SCRIPT_CLASSES: { [string]: boolean } = { Script = true, LocalScript = true, ModuleScript = true, } local function execCreateScript(a: Action): ActionResult if not a.path then return { ok = false, msg = "create_script: missing path" } end local kind = a.scriptType or "Script" if not SCRIPT_CLASSES[kind] then return { ok = false, msg = "create_script: invalid scriptType " .. tostring(kind) } end local parent, leaf, err = resolveParentAndLeaf(a.path, true) if err or not parent or not leaf then return { ok = false, msg = "create_script: " .. tostring(err) } end -- Refuse to overwrite existing instance with the same name if parent:FindFirstChild(leaf) then return { ok = false, msg = "create_script: '" .. leaf .. "' already exists at " .. a.path .. " (use edit_script to replace its source)" } end local script: any = Instance.new(kind) script.Name = leaf script.Source = a.source or "" script.Parent = parent return { ok = true, msg = "created " .. kind .. " " .. a.path } end local function execEditScript(a: Action): ActionResult if not a.path then return { ok = false, msg = "edit_script: missing path" } end local inst, err = resolvePath(a.path, false) if err or not inst then return { ok = false, msg = "edit_script: " .. tostring(err) } end if not inst:IsA("LuaSourceContainer") then return { ok = false, msg = "edit_script: " .. a.path .. " is not a script (got " .. inst.ClassName .. ")" } end local newSource = a.source or "" if ScriptEditorService and ScriptEditorService.UpdateSourceAsync then local ok, err2 = pcall(function() ScriptEditorService:UpdateSourceAsync(inst, function() return newSource end) end) if not ok then -- Fall back to direct .Source assignment (inst :: any).Source = newSource end else (inst :: any).Source = newSource end return { ok = true, msg = "edited " .. a.path } end local function execCreateInstance(a: Action): ActionResult if not a.className or not a.parent or not a.name then return { ok = false, msg = "create_instance: missing className/parent/name" } end local parent, perr = resolvePath(a.parent, true) if perr or not parent then return { ok = false, msg = "create_instance: parent: " .. tostring(perr) } end -- Refuse name collisions if parent:FindFirstChild(a.name) then return { ok = false, msg = "create_instance: '" .. a.name .. "' already exists in " .. a.parent } end local inst local ok, err = pcall(function() inst = Instance.new(a.className :: any) inst.Name = a.name :: string end) if not ok then return { ok = false, msg = "create_instance: " .. tostring(err) } end if a.properties then local perr2 = applyProperties(inst, a.properties) if perr2 then inst:Destroy() return { ok = false, msg = "create_instance: " .. perr2 } end end inst.Parent = parent return { ok = true, msg = "created " .. a.className .. " " .. a.parent .. "." .. a.name } end local function execSetProperty(a: Action): ActionResult if not a.path or not a.properties then return { ok = false, msg = "set_property: missing path or properties" } end local inst, err = resolvePath(a.path, false) if err or not inst then return { ok = false, msg = "set_property: " .. tostring(err) } end local perr = applyProperties(inst, a.properties) if perr then return { ok = false, msg = "set_property: " .. perr } end return { ok = true, msg = "updated properties on " .. a.path } end local function execDeleteInstance(a: Action): ActionResult if not a.path then return { ok = false, msg = "delete_instance: missing path" } end local inst, err = resolvePath(a.path, false) if err or not inst then return { ok = false, msg = "delete_instance: " .. tostring(err) } end inst:Destroy() return { ok = true, msg = "deleted " .. a.path } end local EXECUTORS: { [string]: (Action) -> ActionResult } = { create_script = execCreateScript, edit_script = execEditScript, create_instance = execCreateInstance, set_property = execSetProperty, delete_instance = execDeleteInstance, } -- Run a batch of actions wrapped in a single ChangeHistoryService -- waypoint so Ctrl+Z reverts the whole batch. local function executeActions(actions: { Action }): { ActionResult } local results: { ActionResult } = {} local recId if ChangeHistoryService.TryBeginRecording then recId = ChangeHistoryService:TryBeginRecording("Helios — " .. #actions .. " action(s)", "Helios autonomous edits") end for _, a in ipairs(actions) do local fn = EXECUTORS[a.type] if not fn then table.insert(results, { ok = false, msg = "unknown action type: " .. tostring(a.type) }) else local ok, res = pcall(function() return fn(a) end) if not ok then table.insert(results, { ok = false, msg = "exception: " .. tostring(res) }) else table.insert(results, res) end end end if recId and ChangeHistoryService.FinishRecording then ChangeHistoryService:FinishRecording(recId, Enum.FinishRecordingOperation.Commit) end return results end -- ── Action parser ───────────────────────────────────────────────────── -- Find every {...} block in the response. -- Returns the list of decoded action tables (best-effort — skips -- malformed JSON) AND the response text with the action blocks stripped. local function extractActions(text: string): ({ Action }, string) local actions: { Action } = {} local stripped = text local pattern = "(.-)" while true do local s, e, body = string.find(stripped, pattern) if not s then break end local ok, decoded = pcall(function() return HttpService:JSONDecode(body) end) if ok and type(decoded) == "table" and decoded.type then table.insert(actions, decoded :: any) end stripped = stripped:sub(1, s - 1) .. stripped:sub(e + 1) end -- Trim leftover whitespace stripped = stripped:gsub("^%s+", ""):gsub("%s+$", "") return actions, stripped end -- Pretty one-line summary for an action local function summarizeAction(a: Action): string local r = a.type if a.type == "create_script" then r = string.format("Create %s — %s", a.scriptType or "Script", a.path or "?") elseif a.type == "edit_script" then r = "Edit script — " .. (a.path or "?") elseif a.type == "create_instance" then r = string.format("Create %s '%s' in %s", a.className or "?", a.name or "?", a.parent or "?") elseif a.type == "set_property" then local n = 0 if a.properties then for _ in pairs(a.properties) do n = n + 1 end end r = string.format("Set %d propert%s on %s", n, n == 1 and "y" or "ies", a.path or "?") elseif a.type == "delete_instance" then r = "DELETE " .. (a.path or "?") end return r end -- ── UI ──────────────────────────────────────────────────────────────── print("[Helios v1.5] UI build start") local root = Instance.new("Frame") root.Size = UDim2.fromScale(1, 1) root.BackgroundColor3 = color("MainBackground") root.BorderSizePixel = 0 root.Parent = widget print("[Helios v1.5] root frame parented to widget") -- Header local header = Instance.new("Frame") header.Size = UDim2.new(1, 0, 0, 36) header.BackgroundColor3 = color("Titlebar") header.BorderSizePixel = 0 header.Parent = root local headerLabel = Instance.new("TextLabel") headerLabel.Size = UDim2.new(1, -200, 1, 0) headerLabel.Position = UDim2.fromOffset(12, 0) headerLabel.BackgroundTransparency = 1 headerLabel.Text = "HELIOS" headerLabel.Font = Enum.Font.Code headerLabel.TextSize = 13 headerLabel.TextColor3 = CYAN headerLabel.TextXAlignment = Enum.TextXAlignment.Left headerLabel.Parent = header -- Autopilot toggle local autoBtn = Instance.new("TextButton") autoBtn.Size = UDim2.fromOffset(108, 28) autoBtn.Position = UDim2.new(1, -188, 0, 4) autoBtn.BackgroundColor3 = autopilot and SUCCESS or color("Button") autoBtn.BorderSizePixel = 0 autoBtn.Font = Enum.Font.Code autoBtn.TextSize = 11 autoBtn.TextColor3 = autopilot and Color3.new(0, 0, 0) or color("MainText") autoBtn.Text = autopilot and "AUTOPILOT ON" or "AUTOPILOT OFF" autoBtn.Parent = header local function setAutopilot(on: boolean) autopilot = on plugin:SetSetting(SETTINGS_KEY_AUTOPILOT, on) autoBtn.BackgroundColor3 = on and SUCCESS or color("Button") autoBtn.TextColor3 = on and Color3.new(0, 0, 0) or color("MainText") autoBtn.Text = on and "AUTOPILOT ON" or "AUTOPILOT OFF" end autoBtn.MouseButton1Click:Connect(function() setAutopilot(not autopilot) end) local settingsBtn = Instance.new("TextButton") settingsBtn.Size = UDim2.fromOffset(36, 28) settingsBtn.Position = UDim2.new(1, -76, 0, 4) settingsBtn.BackgroundColor3 = color("Button") settingsBtn.BorderSizePixel = 0 settingsBtn.Text = "API" settingsBtn.Font = Enum.Font.Code settingsBtn.TextSize = 11 settingsBtn.TextColor3 = color("MainText") settingsBtn.Parent = header local clearBtn = Instance.new("TextButton") clearBtn.Size = UDim2.fromOffset(36, 28) clearBtn.Position = UDim2.new(1, -40, 0, 4) clearBtn.BackgroundColor3 = color("Button") clearBtn.BorderSizePixel = 0 clearBtn.Text = "Clr" clearBtn.Font = Enum.Font.Code clearBtn.TextSize = 11 clearBtn.TextColor3 = color("MainText") clearBtn.Parent = header print("[Helios v1.5] header + buttons built") -- Messages area local scroll = Instance.new("ScrollingFrame") scroll.Size = UDim2.new(1, 0, 1, -120) scroll.Position = UDim2.fromOffset(0, 36) scroll.BackgroundColor3 = color("MainBackground") scroll.BorderSizePixel = 0 scroll.ScrollBarThickness = 6 scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y scroll.CanvasSize = UDim2.new() scroll.ScrollingDirection = Enum.ScrollingDirection.Y scroll.Parent = root local layout = Instance.new("UIListLayout") layout.Padding = UDim.new(0, 8) layout.SortOrder = Enum.SortOrder.LayoutOrder layout.Parent = scroll local pad = Instance.new("UIPadding") pad.PaddingTop = UDim.new(0, 12) pad.PaddingBottom = UDim.new(0, 12) pad.PaddingLeft = UDim.new(0, 10) pad.PaddingRight = UDim.new(0, 10) pad.Parent = scroll print("[Helios v1.5] scroll area built") -- Input area local inputFrame = Instance.new("Frame") inputFrame.Size = UDim2.new(1, 0, 0, 84) inputFrame.Position = UDim2.new(0, 0, 1, -84) inputFrame.BackgroundColor3 = color("Titlebar") inputFrame.BorderSizePixel = 0 inputFrame.Parent = root local inputBox = Instance.new("TextBox") inputBox.Size = UDim2.new(1, -90, 1, -16) inputBox.Position = UDim2.fromOffset(8, 8) inputBox.BackgroundColor3 = color("InputFieldBackground") inputBox.BorderSizePixel = 0 inputBox.Text = "" inputBox.PlaceholderText = "Ask Helios to build, fix, or explain…" inputBox.PlaceholderColor3 = color("DimmedText") inputBox.Font = Enum.Font.SourceSans inputBox.TextSize = 14 inputBox.TextColor3 = color("MainText") inputBox.TextXAlignment = Enum.TextXAlignment.Left inputBox.TextYAlignment = Enum.TextYAlignment.Top inputBox.MultiLine = true inputBox.ClearTextOnFocus = false inputBox.TextWrapped = true inputBox.Parent = inputFrame local inputPad = Instance.new("UIPadding") inputPad.PaddingTop = UDim.new(0, 6) inputPad.PaddingBottom = UDim.new(0, 6) inputPad.PaddingLeft = UDim.new(0, 8) inputPad.PaddingRight = UDim.new(0, 8) inputPad.Parent = inputBox local sendBtn = Instance.new("TextButton") sendBtn.Size = UDim2.fromOffset(72, 32) sendBtn.Position = UDim2.new(1, -80, 0.5, -16) sendBtn.BackgroundColor3 = CYAN sendBtn.BorderSizePixel = 0 sendBtn.Text = "Send" sendBtn.Font = Enum.Font.Code sendBtn.TextSize = 13 sendBtn.TextColor3 = Color3.new(0, 0, 0) sendBtn.Parent = inputFrame -- ── Bubble + action card rendering ──────────────────────────────────── local function makeBubble(role: string, text: string, layoutOrder: number): Frame local frame = Instance.new("Frame") frame.Size = UDim2.new(1, -20, 0, 0) frame.AutomaticSize = Enum.AutomaticSize.Y frame.BackgroundColor3 = role == "user" and CYAN:Lerp(color("MainBackground"), 0.85) or color("ScriptBackground") frame.BorderSizePixel = 0 frame.LayoutOrder = layoutOrder local stroke = Instance.new("UIStroke") stroke.Color = role == "user" and CYAN:Lerp(Color3.new(0,0,0), 0.5) or color("Border") stroke.Thickness = 1 stroke.Parent = frame local corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(0, 6) corner.Parent = frame local roleLabel = Instance.new("TextLabel") roleLabel.Size = UDim2.new(1, -16, 0, 16) roleLabel.Position = UDim2.fromOffset(8, 4) roleLabel.BackgroundTransparency = 1 roleLabel.Text = role == "user" and "YOU" or "HELIOS" roleLabel.Font = Enum.Font.Code roleLabel.TextSize = 10 roleLabel.TextColor3 = role == "user" and CYAN or AMBER roleLabel.TextXAlignment = Enum.TextXAlignment.Left roleLabel.Parent = frame local body = Instance.new("TextLabel") body.Size = UDim2.new(1, -16, 0, 0) body.Position = UDim2.fromOffset(8, 22) body.BackgroundTransparency = 1 body.Text = text body.Font = role == "user" and Enum.Font.SourceSans or Enum.Font.Code body.TextSize = 13 body.TextColor3 = color("MainText") body.TextXAlignment = Enum.TextXAlignment.Left body.TextYAlignment = Enum.TextYAlignment.Top body.TextWrapped = true body.AutomaticSize = Enum.AutomaticSize.Y body.RichText = false body.Parent = frame local btmPad = Instance.new("UIPadding") btmPad.PaddingBottom = UDim.new(0, 8) btmPad.Parent = frame return frame end -- Render an action card with Apply / Skip buttons. onApply: function() runs on click. local function makeActionCard(action: Action, layoutOrder: number, onApply: () -> ()): Frame local card = Instance.new("Frame") card.Size = UDim2.new(1, -20, 0, 0) card.AutomaticSize = Enum.AutomaticSize.Y card.BackgroundColor3 = color("ScriptBackground"):Lerp(AMBER, 0.06) card.BorderSizePixel = 0 card.LayoutOrder = layoutOrder local stroke = Instance.new("UIStroke") stroke.Color = AMBER:Lerp(Color3.new(0, 0, 0), 0.4) stroke.Thickness = 1 stroke.Parent = card local corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(0, 6) corner.Parent = card local inset = Instance.new("UIPadding") inset.PaddingTop = UDim.new(0, 8) inset.PaddingBottom = UDim.new(0, 10) inset.PaddingLeft = UDim.new(0, 10) inset.PaddingRight = UDim.new(0, 10) inset.Parent = card local list = Instance.new("UIListLayout") list.Padding = UDim.new(0, 4) list.SortOrder = Enum.SortOrder.LayoutOrder list.Parent = card local heading = Instance.new("TextLabel") heading.Size = UDim2.new(1, 0, 0, 18) heading.BackgroundTransparency = 1 heading.Text = "ACTION · " .. action.type:upper() heading.Font = Enum.Font.Code heading.TextSize = 10 heading.TextColor3 = AMBER heading.TextXAlignment = Enum.TextXAlignment.Left heading.LayoutOrder = 1 heading.Parent = card local summary = Instance.new("TextLabel") summary.Size = UDim2.new(1, 0, 0, 0) summary.AutomaticSize = Enum.AutomaticSize.Y summary.BackgroundTransparency = 1 summary.Text = summarizeAction(action) summary.Font = Enum.Font.SourceSansBold summary.TextSize = 14 summary.TextColor3 = color("MainText") summary.TextXAlignment = Enum.TextXAlignment.Left summary.TextYAlignment = Enum.TextYAlignment.Top summary.TextWrapped = true summary.LayoutOrder = 2 summary.Parent = card if action.rationale and #action.rationale > 0 then local rat = Instance.new("TextLabel") rat.Size = UDim2.new(1, 0, 0, 0) rat.AutomaticSize = Enum.AutomaticSize.Y rat.BackgroundTransparency = 1 rat.Text = action.rationale rat.Font = Enum.Font.SourceSans rat.TextSize = 12 rat.TextColor3 = color("DimmedText") rat.TextXAlignment = Enum.TextXAlignment.Left rat.TextYAlignment = Enum.TextYAlignment.Top rat.TextWrapped = true rat.LayoutOrder = 3 rat.Parent = card end -- Source preview (collapsed) for create_script / edit_script if action.source and #action.source > 0 then local prevWrap = Instance.new("Frame") prevWrap.Size = UDim2.new(1, 0, 0, 0) prevWrap.AutomaticSize = Enum.AutomaticSize.Y prevWrap.BackgroundColor3 = Color3.new(0, 0, 0):Lerp(color("MainBackground"), 0.4) prevWrap.BorderSizePixel = 0 prevWrap.LayoutOrder = 4 prevWrap.Parent = card local prevCorner = Instance.new("UICorner") prevCorner.CornerRadius = UDim.new(0, 4) prevCorner.Parent = prevWrap local prevPad = Instance.new("UIPadding") prevPad.PaddingTop = UDim.new(0, 6) prevPad.PaddingBottom = UDim.new(0, 6) prevPad.PaddingLeft = UDim.new(0, 8) prevPad.PaddingRight = UDim.new(0, 8) prevPad.Parent = prevWrap local prev = Instance.new("TextLabel") prev.Size = UDim2.new(1, 0, 0, 0) prev.AutomaticSize = Enum.AutomaticSize.Y prev.BackgroundTransparency = 1 local src = action.source local lineCount = select(2, src:gsub("\n", "\n")) + 1 local truncated = src if lineCount > 12 then local lines = {} local i = 0 for line in src:gmatch("([^\n]*)\n?") do table.insert(lines, line) i = i + 1 if i >= 10 then break end end truncated = table.concat(lines, "\n") .. string.format("\n… (%d more lines)", lineCount - 10) end prev.Text = truncated prev.Font = Enum.Font.Code prev.TextSize = 11 prev.TextColor3 = color("MainText") prev.TextXAlignment = Enum.TextXAlignment.Left prev.TextYAlignment = Enum.TextYAlignment.Top prev.TextWrapped = false prev.RichText = false prev.Parent = prevWrap end local btnRow = Instance.new("Frame") btnRow.Size = UDim2.new(1, 0, 0, 32) btnRow.BackgroundTransparency = 1 btnRow.LayoutOrder = 5 btnRow.Parent = card local applyBtn = Instance.new("TextButton") applyBtn.Size = UDim2.fromOffset(86, 28) applyBtn.Position = UDim2.fromOffset(0, 2) applyBtn.BackgroundColor3 = AMBER applyBtn.BorderSizePixel = 0 applyBtn.Font = Enum.Font.Code applyBtn.TextSize = 12 applyBtn.TextColor3 = Color3.new(0, 0, 0) applyBtn.Text = "APPLY" applyBtn.Parent = btnRow local skipBtn = Instance.new("TextButton") skipBtn.Size = UDim2.fromOffset(64, 28) skipBtn.Position = UDim2.fromOffset(94, 2) skipBtn.BackgroundColor3 = color("Button") skipBtn.BorderSizePixel = 0 skipBtn.Font = Enum.Font.Code skipBtn.TextSize = 12 skipBtn.TextColor3 = color("MainText") skipBtn.Text = "Skip" skipBtn.Parent = btnRow local statusLabel = Instance.new("TextLabel") statusLabel.Size = UDim2.new(1, -170, 1, 0) statusLabel.Position = UDim2.fromOffset(166, 0) statusLabel.BackgroundTransparency = 1 statusLabel.Text = "" statusLabel.Font = Enum.Font.Code statusLabel.TextSize = 11 statusLabel.TextColor3 = color("DimmedText") statusLabel.TextXAlignment = Enum.TextXAlignment.Left statusLabel.TextYAlignment = Enum.TextYAlignment.Center statusLabel.TextWrapped = true statusLabel.Parent = btnRow local consumed = false local function lockButtons(label: string, color3: Color3) applyBtn.Active = false applyBtn.AutoButtonColor = false applyBtn.BackgroundColor3 = color("Button") applyBtn.TextColor3 = color("DimmedText") applyBtn.Text = label skipBtn.Visible = false statusLabel.TextColor3 = color3 end applyBtn.MouseButton1Click:Connect(function() if consumed then return end consumed = true onApply() end) skipBtn.MouseButton1Click:Connect(function() if consumed then return end consumed = true lockButtons("Skipped", color("DimmedText")) statusLabel.Text = "skipped" end) -- Allow external code to update status post-apply card:SetAttribute("HeliosActionCardId", true) local api = { markApplied = function(msg: string) lockButtons("Applied", SUCCESS) statusLabel.Text = "✓ " .. msg statusLabel.TextColor3 = SUCCESS end, markFailed = function(msg: string) lockButtons("Failed", DANGER) statusLabel.Text = "✗ " .. msg statusLabel.TextColor3 = DANGER end, autoApplyClick = function() applyBtn.MouseButton1Click:Fire() end, } -- Smuggle the API onto the frame so re-renders don't lose it (we -- key by frame identity in the active-cards table). return card, api :: any end -- ── Conversation rendering ──────────────────────────────────────────── local function clearScroll() for _, c in ipairs(scroll:GetChildren()) do if c:IsA("Frame") then c:Destroy() end end end local function appendBubble(role: string, text: string) if not text or #text:gsub("%s", "") == 0 then return end makeBubble(role, text, #scroll:GetChildren()).Parent = scroll end local function appendActionCard(action: Action, onApply: (any) -> ()) local card, api = makeActionCard(action, #scroll:GetChildren(), function() onApply(api) end) card.Parent = scroll if autopilot then task.delay(AUTOPILOT_DELAY_SEC, function() if api and not card:GetAttribute("HeliosConsumed") then card:SetAttribute("HeliosConsumed", true) onApply(api) end end) end end -- Re-render the persistent chat history (text only — actions don't -- persist; they're transient per-turn). local function rerenderHistory() clearScroll() for _, msg in ipairs(conversation) do appendBubble(msg.role, msg.content) end end print("[Helios v1.5] input + send button built — rendering history") rerenderHistory() print("[Helios v1.5] history rendered — UI READY") -- ── HTTP layer ──────────────────────────────────────────────────────── local function selectedScriptContext(): string? local sel = Selection:Get() for _, obj in ipairs(sel) do if obj:IsA("LuaSourceContainer") then local ok, src = pcall(function() return (obj :: any).Source end) if ok and type(src) == "string" and #src > 0 then return string.format( "\n\n=== SELECTED SCRIPT (%s @ %s) ===\n%s", obj.ClassName, obj:GetFullName(), src ) end end end return nil end local function callHelios(userText: string, onComplete: (text: string, actions: { Action }) -> ()) local apiKey = loadApiKey() if not apiKey or #apiKey == 0 then onComplete("⚠ No API key set. Click the API button in the header to add one.\n\nGet a key at https://nyptid.com/keys", {}) return end local messages = {} for _, m in ipairs(conversation) do table.insert(messages, { role = m.role, content = m.content }) end local ctx = selectedScriptContext() local content = userText if ctx then content = userText .. ctx end table.insert(messages, { role = "user", content = content }) local body = HttpService:JSONEncode({ messages = messages, nostream = true, max_tokens = 6000, -- bigger budget for autonomous responses with full script bodies temperature = 0.6, }) local ok, result = pcall(function() return HttpService:RequestAsync({ Url = API_CHAT, Method = "POST", Headers = { ["Content-Type"] = "application/json", ["Authorization"] = "Bearer " .. apiKey, ["User-Agent"] = "Helios-Roblox-Studio-Plugin/1.0", }, Body = body, }) end) if not ok then onComplete("⚠ Network error: " .. tostring(result) .. "\n\nMake sure HttpService is enabled in Game Settings → Security.", {}) return end local response = result :: any if not response.Success then if response.StatusCode == 401 then onComplete("⚠ API key rejected (401). Generate a fresh one at https://nyptid.com/keys and update via the API button.", {}) return end if response.StatusCode == 402 then onComplete("⚠ Free quota exhausted. Upgrade at https://nyptid.com/pricing for unlimited chat.", {}) return end onComplete(string.format("⚠ HTTP %d: %s", response.StatusCode, tostring(response.StatusMessage or response.Body)), {}) return end local parsed local pok, perr = pcall(function() parsed = HttpService:JSONDecode(response.Body) end) if not pok then onComplete("⚠ Failed to parse response: " .. tostring(perr), {}) return end local raw = (parsed and parsed.content) or "(no content)" local actions, stripped = extractActions(raw) onComplete(stripped, actions) end -- ── Wiring ──────────────────────────────────────────────────────────── toggleButton.Click:Connect(function() widget.Enabled = not widget.Enabled toggleButton:SetActive(widget.Enabled) end) widget:GetPropertyChangedSignal("Enabled"):Connect(function() toggleButton:SetActive(widget.Enabled) end) clearBtn.MouseButton1Click:Connect(function() conversation = {} saveHistory() rerenderHistory() end) -- Casey 2026-05-06 v1.5: was an overlay frame parented to `root` with -- ZIndex=10 — but Studio's UI ZIndex sorting tied with the main UI's -- elements (all default Z=1) and the overlay's CHILDREN were not -- guaranteed to render above the chat UI. Casey saw "black screen" -- when clicking API. -- -- New approach: just HIDE the chat UI (header / scroll / inputFrame) and -- show the API config in a fresh Frame. Bulletproof — no Z-index drama. -- On Save/Cancel, destroy the config + un-hide the chat UI. local function showApiSettings() -- Hide chat UI header.Visible = false scroll.Visible = false inputFrame.Visible = false local existing = loadApiKey() or "" local panel = Instance.new("Frame") panel.Size = UDim2.fromScale(1, 1) panel.BackgroundColor3 = color("MainBackground") panel.BorderSizePixel = 0 panel.Parent = root local title = Instance.new("TextLabel") title.Size = UDim2.new(1, -32, 0, 32) title.Position = UDim2.fromOffset(16, 24) title.BackgroundTransparency = 1 title.Text = "Helios API key" title.Font = Enum.Font.Code title.TextSize = 18 title.TextColor3 = CYAN title.TextXAlignment = Enum.TextXAlignment.Left title.Parent = panel local hint = Instance.new("TextLabel") hint.Size = UDim2.new(1, -32, 0, 60) hint.Position = UDim2.fromOffset(16, 64) hint.BackgroundTransparency = 1 hint.Text = "Paste your key from https://nyptid.com/keys\n\nFormat: hel_live_xxxxxxxx..." hint.Font = Enum.Font.SourceSans hint.TextSize = 13 hint.TextColor3 = color("DimmedText") hint.TextXAlignment = Enum.TextXAlignment.Left hint.TextYAlignment = Enum.TextYAlignment.Top hint.TextWrapped = true hint.Parent = panel local inputLabel = Instance.new("TextLabel") inputLabel.Size = UDim2.new(1, -32, 0, 16) inputLabel.Position = UDim2.fromOffset(16, 138) inputLabel.BackgroundTransparency = 1 inputLabel.Text = "API KEY" inputLabel.Font = Enum.Font.Code inputLabel.TextSize = 11 inputLabel.TextColor3 = AMBER inputLabel.TextXAlignment = Enum.TextXAlignment.Left inputLabel.Parent = panel local input = Instance.new("TextBox") input.Size = UDim2.new(1, -32, 0, 36) input.Position = UDim2.fromOffset(16, 158) input.BackgroundColor3 = color("InputFieldBackground") input.BorderSizePixel = 1 input.BorderColor3 = CYAN input.Text = existing input.PlaceholderText = "hel_live_..." input.PlaceholderColor3 = color("DimmedText") input.Font = Enum.Font.Code input.TextSize = 13 input.TextColor3 = color("MainText") input.TextXAlignment = Enum.TextXAlignment.Left input.ClearTextOnFocus = false input.Parent = panel local saveBtn = Instance.new("TextButton") saveBtn.Size = UDim2.fromOffset(100, 36) saveBtn.Position = UDim2.fromOffset(16, 210) saveBtn.BackgroundColor3 = CYAN saveBtn.BorderSizePixel = 0 saveBtn.Text = "Save" saveBtn.Font = Enum.Font.Code saveBtn.TextSize = 14 saveBtn.TextColor3 = Color3.new(0, 0, 0) saveBtn.Parent = panel local cancelBtn = Instance.new("TextButton") cancelBtn.Size = UDim2.fromOffset(100, 36) cancelBtn.Position = UDim2.fromOffset(124, 210) cancelBtn.BackgroundColor3 = color("Button") cancelBtn.BorderSizePixel = 0 cancelBtn.Text = "Cancel" cancelBtn.Font = Enum.Font.Code cancelBtn.TextSize = 14 cancelBtn.TextColor3 = color("MainText") cancelBtn.Parent = panel local status = Instance.new("TextLabel") status.Size = UDim2.new(1, -32, 0, 24) status.Position = UDim2.fromOffset(16, 254) status.BackgroundTransparency = 1 status.Text = "" status.Font = Enum.Font.SourceSans status.TextSize = 12 status.TextColor3 = SUCCESS status.TextXAlignment = Enum.TextXAlignment.Left status.Parent = panel local function close() panel:Destroy() header.Visible = true scroll.Visible = true inputFrame.Visible = true end saveBtn.MouseButton1Click:Connect(function() local k = input.Text if not k or #k:gsub("%s", "") == 0 then status.Text = "⚠ Empty key — paste it before saving" status.TextColor3 = DANGER return end saveApiKey(k) status.Text = "✓ Saved. Closing..." status.TextColor3 = SUCCESS task.delay(0.6, close) end) cancelBtn.MouseButton1Click:Connect(close) end settingsBtn.MouseButton1Click:Connect(showApiSettings) local function send() if sending then return end local text = inputBox.Text if not text or #text:gsub("%s", "") == 0 then return end sending = true sendBtn.Text = "..." table.insert(conversation, { role = "user", content = text }) inputBox.Text = "" saveHistory() rerenderHistory() -- Pending bubble local pendingIdx = #conversation + 1 table.insert(conversation, { role = "assistant", content = "thinking…" }) rerenderHistory() task.spawn(function() callHelios(text, function(stripped, actions) -- Replace pending with the actual prose response conversation[pendingIdx] = { role = "assistant", content = stripped } saveHistory() rerenderHistory() -- Append action cards (transient, not stored in history) for _, action in ipairs(actions) do appendActionCard(action, function(api) local results = executeActions({ action }) local r = results[1] if r and r.ok then api.markApplied(r.msg) else api.markFailed((r and r.msg) or "unknown error") end end) end -- Summary footer when there were actions if #actions > 0 then local note = string.format( "%d action%s queued. %s", #actions, #actions == 1 and "" or "s", autopilot and "Autopilot ON — applying automatically." or "Click APPLY on each (or toggle AUTOPILOT)." ) local foot = Instance.new("TextLabel") foot.Size = UDim2.new(1, -20, 0, 18) foot.LayoutOrder = #scroll:GetChildren() foot.BackgroundTransparency = 1 foot.Text = note foot.Font = Enum.Font.Code foot.TextSize = 10 foot.TextColor3 = AMBER foot.TextXAlignment = Enum.TextXAlignment.Left foot.Parent = scroll end sending = false sendBtn.Text = "Send" end) end) end sendBtn.MouseButton1Click:Connect(send) inputBox.FocusLost:Connect(function(enterPressed) if enterPressed and not ( game:GetService("UserInputService"):IsKeyDown(Enum.KeyCode.LeftShift) or game:GetService("UserInputService"):IsKeyDown(Enum.KeyCode.RightShift) ) then send() end end) -- Boot log — visible in View → Output. Help diagnose any future -- "plugin loaded but UI didn't render" cases. print(string.format( "[Helios v1.5] plugin loaded · API key: %s · click 'Helios' in Plugins tab to open the dock", tostring(loadApiKey() ~= nil) ))