--!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)
))