local HttpService = game:GetService("HttpService")
local Selection = game:GetService("Selection")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local TweenService = game:GetService("TweenService")
local AssetService = game:GetService("AssetService")
local LogService = game:GetService("LogService")
local API = "https://sculpt.run"
local VERSION = "Sculpt v3.1"
pcall(function() HttpService.HttpEnabled = true end)
local function safeFont(name, fallback)
local ok, f = pcall(function() return Enum.Font[name] end)
return ok and f or fallback
end
local FONT = safeFont("BuilderSans", Enum.Font.Gotham)
local FONT_MED = safeFont("BuilderSansMedium", Enum.Font.GothamMedium)
local FONT_BOLD = safeFont("BuilderSansBold", Enum.Font.GothamBold)
local FONT_MONO = safeFont("RobotoMono", Enum.Font.Code)
local MODELS = {
{ id = "claude-fable-5", label = "Fable 5", vendor = "anthropic", mult = 3.3 },
{ id = "claude-opus-4-8", label = "Opus 4.8", vendor = "anthropic", mult = 1.7 },
{ id = "claude-opus-4-7", label = "Opus 4.7", vendor = "anthropic", mult = 1.7 },
{ id = "claude-sonnet-5", label = "Sonnet 5", vendor = "anthropic", mult = 0.7 },
{ id = "claude-sonnet-4-6", label = "Sonnet 4.6", vendor = "anthropic", mult = 1 },
{ id = "claude-haiku-4-5-20251001", label = "Haiku 4.5", vendor = "anthropic", mult = 0.35 },
{ id = "gpt-5.5", label = "GPT-5.5", vendor = "openai", mult = 1.2 },
{ id = "gpt-5.6-sol", label = "GPT-5.6 Sol", vendor = "openai", mult = 2 },
{ id = "gpt-5.6-terra", label = "GPT-5.6 Terra", vendor = "openai", mult = 2 },
{ id = "gpt-5.6-luna", label = "GPT-5.6 Luna", vendor = "openai", mult = 2 },
}
local EFFORTS = { "low", "medium", "high", "xhigh", "max" }
local CREDIT_TOKENS = 100000
local EST_OUT_TOKENS = { low = 800, medium = 1600, high = 3200, xhigh = 5000, max = 8000 }
local function fmtCr(n)
if n >= 100 then return string.format("%d", math.floor(n + 0.5)) end
return (string.format("%.2f", n):gsub("%.?0+$", ""))
end
local QUICK_ACTIONS = {
{ icon = "π", label = "Find bugs", prompt = "Find bugs in the selected scripts and explain each one with the line it lives on." },
{ icon = "β¨", label = "Refactor this", prompt = "Refactor the selected scripts: cleaner structure, better names, same behavior." },
{ icon = "π", label = "Explain code", prompt = "Explain what the selected scripts do, step by step, in plain language." },
{ icon = "β‘", label = "Optimize", prompt = "Optimize the selected scripts for performance and memory. Point out every hot spot you change." },
}
local SLASH_PROMPTS = {
explain = "Explain what the selected scripts do, plainly.",
optimize = "Find performance problems in the selected scripts and show fixes.",
tests = "Write test cases for the selected code.",
}
local SLASH_HINT = "/explain Β· /fix Β· /optimize Β· /tests"
local function save(k, v) plugin:SetSetting("sculpt_" .. k, v) end
local function load(k) return plugin:GetSetting("sculpt_" .. k) end
local token = load("token") or ""
local chatId = load("chat_id") or 0
local modelId = load("model") or "claude-sonnet-4-6"
local effortId = load("effort") or "medium"
local themeName = load("theme") or "dark"
local thinkOn = load("think"); if thinkOn == nil then thinkOn = true end
local autoApply = load("autoapply"); if autoApply == nil then autoApply = false end
local fontScale = load("fontscale") or 1.15
local sendOnEnter = load("sendenter"); if sendOnEnter == nil then sendOnEnter = true end
local compactMode = load("compact"); if compactMode == nil then compactMode = false end
local FONT_SCALES = {
{ label = "Small", v = 1 },
{ label = "Normal", v = 1.15 },
{ label = "Large", v = 1.3 },
{ label = "XL", v = 1.5 },
}
local scaledTexts = setmetatable({}, { __mode = "k" })
local function scaled(l, base)
scaledTexts[l] = base
l.TextSize = math.floor(base * fontScale + 0.5)
return l
end
local function applyFontScale()
for l, base in pairs(scaledTexts) do
if l.Parent then l.TextSize = math.floor(base * fontScale + 0.5) end
end
end
local CORAL = Color3.fromRGB(255, 107, 122)
local PEACH = Color3.fromRGB(255, 165, 116)
local VIOLET = Color3.fromRGB(167, 139, 250)
local SKY = Color3.fromRGB(125, 211, 252)
local LIME = Color3.fromRGB(190, 242, 100)
local ERR = Color3.fromRGB(248, 113, 113)
local GREEN = Color3.fromRGB(74, 222, 128)
local WHITE = Color3.fromRGB(255, 255, 255)
local INK = Color3.fromRGB(10, 8, 18)
local BTN_TEXT = Color3.fromRGB(26, 5, 16)
local THEMES = {
dark = {
bg = Color3.fromRGB(10, 8, 18),
bg2 = Color3.fromRGB(16, 13, 28),
bg3 = Color3.fromRGB(26, 23, 37),
panel = Color3.fromRGB(17, 15, 26),
line = Color3.fromRGB(30, 28, 37),
line2 = Color3.fromRGB(45, 43, 54),
text = Color3.fromRGB(245, 241, 232),
dim = Color3.fromRGB(162, 156, 184),
mute = Color3.fromRGB(107, 100, 133),
inputBg = Color3.fromRGB(20, 18, 28),
pillBg = Color3.fromRGB(23, 21, 31),
pillHover = Color3.fromRGB(34, 31, 45),
ghostBg = Color3.fromRGB(23, 21, 31),
ghostHover = Color3.fromRGB(36, 33, 48),
menuBg = Color3.fromRGB(15, 12, 25),
menuHover = Color3.fromRGB(32, 29, 45),
codeBg = Color3.fromRGB(7, 6, 13),
codeText = Color3.fromRGB(190, 210, 250),
chipBg = Color3.fromRGB(14, 12, 22),
userBubble = Color3.fromRGB(23, 21, 31),
thinkOnBg = Color3.fromRGB(42, 56, 30),
thinkOnText = LIME,
inlineCode = "#C4B5FD",
annotMix = 0.16,
auroraAlpha = 0.93,
},
light = {
bg = Color3.fromRGB(250, 248, 244),
bg2 = Color3.fromRGB(243, 240, 234),
bg3 = Color3.fromRGB(235, 231, 223),
panel = Color3.fromRGB(255, 255, 255),
line = Color3.fromRGB(230, 228, 224),
line2 = Color3.fromRGB(210, 208, 205),
text = Color3.fromRGB(26, 22, 37),
dim = Color3.fromRGB(107, 100, 133),
mute = Color3.fromRGB(155, 148, 176),
inputBg = Color3.fromRGB(255, 255, 255),
pillBg = Color3.fromRGB(255, 255, 255),
pillHover = Color3.fromRGB(241, 237, 230),
ghostBg = Color3.fromRGB(255, 255, 255),
ghostHover = Color3.fromRGB(241, 237, 230),
menuBg = Color3.fromRGB(255, 255, 255),
menuHover = Color3.fromRGB(243, 240, 234),
codeBg = Color3.fromRGB(24, 20, 34),
codeText = Color3.fromRGB(200, 214, 246),
chipBg = Color3.fromRGB(240, 237, 230),
userBubble = Color3.fromRGB(255, 243, 240),
thinkOnBg = Color3.fromRGB(229, 240, 196),
thinkOnText = Color3.fromRGB(63, 98, 18),
inlineCode = "#6D28D9",
annotMix = 0.10,
auroraAlpha = 0.97,
},
}
local THEME = THEMES[themeName] or THEMES.dark
local themeBindings = {}
local function themed(inst, prop, key)
inst[prop] = THEME[key]
table.insert(themeBindings, { inst = inst, prop = prop, key = key })
return inst
end
local function themedFn(inst, prop, fn)
inst[prop] = fn(THEME)
table.insert(themeBindings, { inst = inst, prop = prop, fn = fn })
return inst
end
local function applyTheme(name)
themeName = name
THEME = THEMES[name] or THEMES.dark
local live = {}
for _, b in ipairs(themeBindings) do
if b.inst.Parent then
pcall(function()
b.inst[b.prop] = if b.fn then b.fn(THEME) else THEME[b.key]
end)
table.insert(live, b)
end
end
themeBindings = live
end
local function applyColor(inst, prop, c)
if type(c) == "string" then themed(inst, prop, c) else inst[prop] = c end
end
local function corner(i, r)
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, r or 8)
c.Parent = i
return c
end
local function stroke(i, col, t, tr)
local s = Instance.new("UIStroke")
applyColor(s, "Color", col)
s.Thickness = t or 1
s.Transparency = tr or 0
s.Parent = i
return s
end
local function pad(i, l, r, t, b)
local p = Instance.new("UIPadding")
p.PaddingLeft = UDim.new(0, l); p.PaddingRight = UDim.new(0, r or l)
p.PaddingTop = UDim.new(0, t or l); p.PaddingBottom = UDim.new(0, b or t or l)
p.Parent = i
end
local function frame(parent, sz, pos, bg)
local f = Instance.new("Frame")
f.Size = sz; f.Position = pos or UDim2.new(0, 0, 0, 0)
f.BorderSizePixel = 0
if bg then applyColor(f, "BackgroundColor3", bg) else f.BackgroundTransparency = 1 end
f.Parent = parent
return f
end
local function text(parent, str, size, col, sz, pos, font, align)
local l = Instance.new("TextLabel")
l.Text = str
scaled(l, size)
applyColor(l, "TextColor3", col or "text")
l.BackgroundTransparency = 1; l.Font = font or FONT
l.TextXAlignment = align or Enum.TextXAlignment.Left
l.TextYAlignment = Enum.TextYAlignment.Top
l.TextWrapped = true
l.Size = sz or UDim2.new(1, 0, 0, math.floor(size * fontScale + 0.5) + 4); l.Position = pos or UDim2.new(0, 0, 0, 0)
l.Parent = parent
return l
end
local function gradient(inst, c1, c2, rot)
local g = Instance.new("UIGradient")
g.Color = ColorSequence.new(c1, c2)
g.Rotation = rot or 135
g.Parent = inst
return g
end
local function hoverable(btn, normalKey, hoverKey)
btn.AutoButtonColor = false
themed(btn, "BackgroundColor3", normalKey)
btn.MouseEnter:Connect(function() btn.BackgroundColor3 = THEME[hoverKey] end)
btn.MouseLeave:Connect(function() btn.BackgroundColor3 = THEME[normalKey] end)
end
local function primaryBtn(parent, str, sz, pos)
local b = Instance.new("TextButton")
b.Text = str; b.TextSize = 14; b.TextColor3 = BTN_TEXT
b.BackgroundColor3 = WHITE; b.BorderSizePixel = 0
b.Font = FONT_BOLD; b.AutoButtonColor = false
b.Size = sz; b.Position = pos or UDim2.new(0, 0, 0, 0)
b.Parent = parent
corner(b, math.floor(sz.Y.Offset / 2))
gradient(b, CORAL, PEACH, 135)
b.MouseEnter:Connect(function() b.BackgroundColor3 = Color3.fromRGB(232, 232, 232) end)
b.MouseLeave:Connect(function() b.BackgroundColor3 = WHITE end)
return b
end
local function ghostBtn(parent, str, sz, pos)
local b = Instance.new("TextButton")
b.Text = str; b.TextSize = 12
themed(b, "TextColor3", "text")
b.BorderSizePixel = 0
b.Font = FONT_MED
b.Size = sz; b.Position = pos or UDim2.new(0, 0, 0, 0)
b.Parent = parent
corner(b, math.floor(sz.Y.Offset / 2))
stroke(b, "line2", 1)
hoverable(b, "ghostBg", "ghostHover")
return b
end
local logoMarks = {}
local function brandMark(parent, size, pos)
local m = frame(parent, UDim2.fromOffset(size, size), pos, WHITE)
corner(m, math.floor(size * 0.27))
local g = Instance.new("UIGradient")
g.Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, CORAL),
ColorSequenceKeypoint.new(0.5, PEACH),
ColorSequenceKeypoint.new(1, VIOLET),
})
g.Rotation = 135
g.Parent = m
table.insert(logoMarks, m)
return m
end
local function textButton(parent, str, sz, pos, bg, col)
local b = Instance.new("TextButton")
b.Text = str; b.TextSize = 13; b.TextColor3 = col or WHITE
b.BackgroundColor3 = bg or VIOLET; b.BorderSizePixel = 0
b.Font = FONT_MED; b.AutoButtonColor = true
b.Size = sz; b.Position = pos or UDim2.new(0, 0, 0, 0)
b.Parent = parent
corner(b, 8)
return b
end
local B64 = {}
do
local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
for i = 1, 64 do B64[chars:byte(i)] = i - 1 end
end
local function b64ToBuffer(s, expectedLen)
local buf = buffer.create(expectedLen)
local acc, bits, n = 0, 0, 0
for i = 1, #s do
local v = B64[s:byte(i)]
if v then
acc = acc * 64 + v
bits += 6
if bits >= 8 then
bits -= 8
if n >= expectedLen then return nil end
buffer.writeu8(buf, n, math.floor(acc / 2 ^ bits))
acc %= 2 ^ bits
n += 1
end
end
end
if n ~= expectedLen then return nil end
return buf
end
local showPair
local function request(method, path_, body)
local headers = { ["Content-Type"] = "application/json" }
if token ~= "" then headers["Authorization"] = "Bearer " .. token end
local ok, res = pcall(function()
return HttpService:RequestAsync({ Url = API .. path_, Method = method, Headers = headers,
Body = body and HttpService:JSONEncode(body) or nil })
end)
if not ok then
local m = tostring(res)
if m:find("Http requests") or m:find("HttpEnabled") then
m = "HTTP off. Game Settings β Security β Allow HTTP Requests"
end
return nil, m
end
local data
if res.Body and res.Body ~= "" then
local ok2, d = pcall(function() return HttpService:JSONDecode(res.Body) end)
if ok2 then data = d end
end
if not res.Success then
if res.StatusCode == 401 and token ~= "" then
token = ""; save("token", "")
task.defer(function() if showPair then showPair() end end)
return nil, "Signed out β pair again"
end
return nil, (data and data.error) or ("Error " .. res.StatusCode)
end
if not data then return {} end
return data
end
local function isProtected(inst)
local c = inst
while c do
if c.Name == "CorePackages" or c.ClassName == "CoreGui" then return true end
c = c.Parent
end
return false
end
local function getSource(scr)
if isProtected(scr) then return nil end
local ok, s = pcall(function() return ScriptEditorService:GetEditorSource(scr) end)
if ok and s then return s end
return scr.Source
end
local function setSource(scr, newSrc)
if isProtected(scr) then return false, "protected" end
local ok, err = pcall(function()
ScriptEditorService:UpdateSourceAsync(scr, function() return newSrc end)
end)
if not ok then
if not pcall(function() scr.Source = newSrc end) then return false, tostring(err) end
end
return true
end
local function diffStats(a, b)
local al = {}
for l in (a .. "\n"):gmatch("(.-)\n") do al[l] = (al[l] or 0) + 1 end
local nA, nB = 0, 0
for _ in (a .. "\n"):gmatch("(.-)\n") do nA += 1 end
local kept = 0
for l in (b .. "\n"):gmatch("(.-)\n") do
nB += 1
if al[l] and al[l] > 0 then al[l] -= 1; kept += 1 end
end
return math.max(0, nB - kept), math.max(0, nA - kept)
end
local StudioService = game:GetService("StudioService")
local function activeEditorScript()
local ok, scr = pcall(function() return StudioService.ActiveScript end)
if ok and scr and scr:IsA("LuaSourceContainer") and not isProtected(scr) then return scr end
return nil
end
local function collectContext()
local sel = Selection:Get()
local parts, targets = {}, {}
for _, inst in ipairs(sel) do
if inst:IsA("LuaSourceContainer") then
local src = getSource(inst)
if src then
table.insert(parts, ("")
:format(inst.Name, inst.ClassName, inst:GetFullName(), src))
table.insert(targets, inst)
end
end
end
if #parts == 0 then
local scr = activeEditorScript()
if scr then
local src = getSource(scr)
if src then
return ("OPEN SCRIPT (active editor tab):\n")
:format(scr.Name, scr.ClassName, scr:GetFullName(), src), { scr }
end
end
return nil, {}
end
return "SELECTED SCRIPTS:\n" .. table.concat(parts, "\n\n"), targets
end
local function collectOutputErrors()
local ok, hist = pcall(function() return LogService:GetLogHistory() end)
if not ok or type(hist) ~= "table" then return nil end
local lines = {}
for i = #hist, 1, -1 do
local e = hist[i]
if e.messageType == Enum.MessageType.MessageError or e.messageType == Enum.MessageType.MessageWarning then
local tag = e.messageType == Enum.MessageType.MessageError and "[ERROR] " or "[WARN] "
table.insert(lines, 1, tag .. tostring(e.message))
if #lines >= 8 then break end
end
end
if #lines == 0 then return nil end
return table.concat(lines, "\n")
end
local function extractCode(str)
local blocks = {}
for _, code in str:gmatch("```(%w*)%s*\n(.-)```") do
code = code:match("^(.-)%s*$")
local entry = { code = code }
local firstLine, rest = code:match("^(.-)\n(.*)$")
firstLine = firstLine or code
local scriptName = firstLine:match("^%-%-%s*script:%s*(.+)%s*$")
local newName, newClass, newParent = firstLine:match("^%-%-%s*new:%s*(.-)%s*|%s*(.-)%s*|%s*(.+)%s*$")
if scriptName then
entry.target = scriptName
entry.code = rest or ""
elseif newName then
entry.newName = newName
entry.newClass = newClass
entry.newParent = newParent
entry.code = rest or ""
end
table.insert(blocks, entry)
end
return blocks
end
local function resolveParent(path_)
local node = game
for seg in path_:gmatch("[^%.]+") do
if seg ~= "game" then
local nxt = node:FindFirstChild(seg)
if not nxt and node == game then
local ok, svc = pcall(function() return game:GetService(seg) end)
if ok then nxt = svc end
end
if not nxt then return nil end
node = nxt
end
end
return node
end
local VALID_SCRIPT_CLASSES = { Script = true, LocalScript = true, ModuleScript = true }
local toolbar = plugin:CreateToolbar("Sculpt")
local toggleBtn = toolbar:CreateButton("SculptToggle", "Open Sculpt", "", "Sculpt")
toggleBtn.ClickableWhenViewportHidden = true
local widget = plugin:CreateDockWidgetPluginGui("SculptPanel",
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 380, 620, 320, 460))
widget.Title = "Sculpt"
toggleBtn.Click:Connect(function()
widget.Enabled = not widget.Enabled
toggleBtn:SetActive(widget.Enabled)
end)
plugin.Unloading:Connect(function() widget.Enabled = false end)
local root = frame(widget, UDim2.new(1, 0, 1, 0), nil, "bg")
root.ClipsDescendants = true
local function blob(px, py, size, col)
local function ring(sz, extra)
local r = frame(root, UDim2.fromOffset(sz, sz), UDim2.new(px, -sz // 2, py, -sz // 2), col)
r.ZIndex = 0
themedFn(r, "BackgroundTransparency", function(t)
return math.min(0.985, t.auroraAlpha + extra)
end)
corner(r, sz // 2)
end
ring(size, 0)
ring(size + 60, 0.035)
ring(size + 120, 0.07)
end
blob(0, 0, 240, CORAL)
blob(1, 0.25, 280, VIOLET)
blob(0.35, 1, 220, SKY)
local pairScreen = frame(root, UDim2.new(1, 0, 1, 0))
local chatScreen = frame(root, UDim2.new(1, 0, 1, 0))
chatScreen.Visible = false
local overlay = frame(root, UDim2.new(1, 0, 1, 0))
overlay.ZIndex = 50
overlay.Visible = false
showPair = function() pairScreen.Visible = true; chatScreen.Visible = false end
local function utf8Trunc(s, maxChars)
local ok, res = pcall(function()
local n = utf8.len(s)
if not n or n <= maxChars then return s end
return s:sub(1, utf8.offset(s, maxChars + 1) - 1) .. "β¦"
end)
return ok and res or s:sub(1, maxChars)
end
local function showChat() pairScreen.Visible = false; chatScreen.Visible = true end
do
pad(pairScreen, 26, 26, 44, 26)
brandMark(pairScreen, 24, UDim2.new(0, 0, 0, 2))
text(pairScreen, "Sculpt", 21, "text", UDim2.new(1, -34, 0, 28), UDim2.new(0, 34, 0, 0), FONT_BOLD)
text(pairScreen, "AI copilot for Roblox Studio", 14, "dim", UDim2.new(1, 0, 0, 20), UDim2.new(0, 0, 0, 42))
text(pairScreen, "π Pair with your account", 16, "text", UDim2.new(1, 0, 0, 22), UDim2.new(0, 0, 0, 84), FONT_BOLD)
text(pairScreen, "1. Open localhost:3000/dashboard\n2. Click \"Get pair code\"\n3. Paste code below", 13.5, "dim", UDim2.new(1, 0, 0, 70), UDim2.new(0, 0, 0, 112))
text(pairScreen, "β Enable HTTP: Game Settings β Security", 12, PEACH, UDim2.new(1, 0, 0, 20), UDim2.new(0, 0, 0, 186))
local codeBox = Instance.new("TextBox")
codeBox.PlaceholderText = "000000"; codeBox.Text = ""
codeBox.TextSize = 28
themed(codeBox, "TextColor3", "text")
themed(codeBox, "PlaceholderColor3", "mute")
themed(codeBox, "BackgroundColor3", "bg3")
codeBox.BorderSizePixel = 0
codeBox.Font = FONT_BOLD; codeBox.ClearTextOnFocus = false
codeBox.TextXAlignment = Enum.TextXAlignment.Center
codeBox.Size = UDim2.new(1, 0, 0, 54); codeBox.Position = UDim2.new(0, 0, 0, 218)
corner(codeBox, 16); stroke(codeBox, "line2", 1); codeBox.Parent = pairScreen
local status = text(pairScreen, "", 13, "dim", UDim2.new(1, 0, 0, 36), UDim2.new(0, 0, 0, 282))
local connectBtn = primaryBtn(pairScreen, "β¨ Connect", UDim2.new(1, 0, 0, 46), UDim2.new(0, 0, 0, 322))
local foot = text(pairScreen, VERSION, 11, "mute", UDim2.new(1, 0, 0, 16), UDim2.new(0, 0, 1, -20), FONT_MONO, Enum.TextXAlignment.Center)
foot.TextYAlignment = Enum.TextYAlignment.Center
connectBtn.MouseButton1Click:Connect(function()
local code = codeBox.Text:match("^%s*(.-)%s*$")
if #code ~= 6 or not code:match("^%d+$") then
status.Text = "Enter 6-digit code"; status.TextColor3 = ERR; return
end
connectBtn.Text = "Connectingβ¦"; status.Text = ""
task.spawn(function()
local data, err = request("POST", "/api/plugin/pair", { code = code })
if not data or not data.token then
status.Text = err or "Pair failed"; status.TextColor3 = ERR
connectBtn.Text = "β¨ Connect"; return
end
token = data.token; save("token", token); chatId = 0; save("chat_id", 0)
status.Text = "β Connected @" .. (data.username or "?"); status.TextColor3 = LIME
connectBtn.Text = "β¨ Connect"
task.wait(0.5); showChat()
if refreshMe then refreshMe() end
end)
end)
end
local scroll, inputBox, sendBtn, sendGrad, runsLabel
local modelBtn, effortBtn, thinkBtn, ctxLabel, quickRow
local loadChat
local refreshQuickActions
local adoptServerModels
local doSend
local refreshChips
local refreshMe
local pendingImages = {}
local active = nil
local msgCount = 0
local welcomeShown = false
local TOP_H, COMP_H = 40, 154
do
local top = frame(chatScreen, UDim2.new(1, 0, 0, TOP_H), UDim2.new(0, 0, 0, 0), "bg2")
stroke(top, "line", 1, 0.75)
brandMark(top, 18, UDim2.new(0, 12, 0.5, -9))
local brandTxt = text(top, "Sculpt", 15, "text", UDim2.new(0, 70, 1, 0), UDim2.new(0, 38, 0, 0), FONT_BOLD)
brandTxt.TextYAlignment = Enum.TextYAlignment.Center
runsLabel = text(top, "β cr", 11, "dim", UDim2.new(0, 70, 1, 0), UDim2.new(1, -230, 0, 0), FONT_MONO, Enum.TextXAlignment.Right)
runsLabel.TextYAlignment = Enum.TextYAlignment.Center
local themeBtn = ghostBtn(top, themeName == "dark" and "π" or "βοΈ", UDim2.new(0, 30, 0, 30), UDim2.new(1, -154, 0.5, -15))
themeBtn.TextSize = 13
local histBtn = ghostBtn(top, "π", UDim2.new(0, 30, 0, 30), UDim2.new(1, -120, 0.5, -15))
histBtn.TextSize = 13
local newBtn = ghostBtn(top, "+ New", UDim2.new(0, 48, 0, 28), UDim2.new(1, -86, 0.5, -14))
newBtn.TextSize = 11
local gearBtn = ghostBtn(top, "β", UDim2.new(0, 30, 0, 30), UDim2.new(1, -34, 0.5, -15))
gearBtn.TextSize = 14
themeBtn.MouseButton1Click:Connect(function()
local nxt = themeName == "dark" and "light" or "dark"
applyTheme(nxt)
save("theme", nxt)
themeBtn.Text = nxt == "dark" and "π" or "βοΈ"
end)
local logFrame = frame(chatScreen, UDim2.new(1, 0, 1, -TOP_H - COMP_H), UDim2.new(0, 0, 0, TOP_H))
logFrame.ClipsDescendants = true
scroll = Instance.new("ScrollingFrame")
scroll.Size = UDim2.new(1, 0, 1, 0); scroll.BackgroundTransparency = 1; scroll.BorderSizePixel = 0
scroll.ScrollBarThickness = 4
themed(scroll, "ScrollBarImageColor3", "line")
scroll.CanvasSize = UDim2.new(0, 0, 0, 0); scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
scroll.ScrollingDirection = Enum.ScrollingDirection.Y; scroll.Parent = logFrame
local msgLayout = Instance.new("UIListLayout"); msgLayout.Padding = UDim.new(0, 16)
msgLayout.SortOrder = Enum.SortOrder.LayoutOrder; msgLayout.Parent = scroll
pad(scroll, 12, 12, 12, 12)
quickRow = frame(logFrame, UDim2.new(1, -24, 0, 150), UDim2.new(0, 12, 1, -166))
quickRow.ZIndex = 2
local qGrid = Instance.new("UIGridLayout")
qGrid.CellSize = UDim2.new(0.5, -5, 0, 32)
qGrid.CellPadding = UDim2.new(0, 10, 0, 8)
qGrid.SortOrder = Enum.SortOrder.LayoutOrder
qGrid.Parent = quickRow
for i, qa in ipairs(QUICK_ACTIONS) do
local chip = Instance.new("TextButton")
chip.Text = qa.icon .. " " .. qa.label
chip.TextSize = 12
themed(chip, "TextColor3", "text")
chip.BorderSizePixel = 0
chip.Font = FONT_MED
chip.LayoutOrder = i
chip.ZIndex = 2
chip.Parent = quickRow
corner(chip, 16); stroke(chip, "line2", 1)
hoverable(chip, "pillBg", "pillHover")
chip.MouseButton1Click:Connect(function()
inputBox.Text = qa.prompt
inputBox:CaptureFocus()
end)
end
refreshQuickActions = function()
quickRow.Visible = msgCount == 0 or (welcomeShown and msgCount == 1)
end
local comp = frame(chatScreen, UDim2.new(1, 0, 0, COMP_H), UDim2.new(0, 0, 1, -COMP_H), "bg2")
stroke(comp, "line", 1, 0.75)
local inputWrap = frame(comp, UDim2.new(1, -24, 0, 52), UDim2.new(0, 12, 0, 12), "inputBg")
corner(inputWrap, 14)
local inS = stroke(inputWrap, "line2", 1)
pad(inputWrap, 12, 12, 8, 8)
inputBox = Instance.new("TextBox")
inputBox.PlaceholderText = "Ask Sculpt β select scripts for context, image URL = reference"; inputBox.Text = ""
scaled(inputBox, 14)
themed(inputBox, "TextColor3", "text")
themed(inputBox, "PlaceholderColor3", "mute")
inputBox.BackgroundTransparency = 1; inputBox.BorderSizePixel = 0
inputBox.Font = FONT; inputBox.ClearTextOnFocus = false; inputBox.MultiLine = true
inputBox.TextXAlignment = Enum.TextXAlignment.Left; inputBox.TextYAlignment = Enum.TextYAlignment.Top
inputBox.TextWrapped = true; inputBox.Size = UDim2.new(1, 0, 1, 0); inputBox.Parent = inputWrap
inputBox.Focused:Connect(function() inS.Color = VIOLET end)
inputBox.FocusLost:Connect(function() inS.Color = THEME.line2 end)
local UserInputService = game:GetService("UserInputService")
inputBox:GetPropertyChangedSignal("Text"):Connect(function()
if not sendOnEnter then return end
local t = inputBox.Text
if t:sub(-1) ~= "\n" then return end
if UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) or UserInputService:IsKeyDown(Enum.KeyCode.RightShift) then return end
inputBox.Text = t:sub(1, -2)
if doSend and not active and inputBox.Text:match("%S") then doSend() end
end)
local pillRow = frame(comp, UDim2.new(1, -24, 0, 26), UDim2.new(0, 12, 0, 70))
local prl = Instance.new("UIListLayout")
prl.FillDirection = Enum.FillDirection.Horizontal
prl.HorizontalAlignment = Enum.HorizontalAlignment.Center
prl.VerticalAlignment = Enum.VerticalAlignment.Center
prl.SortOrder = Enum.SortOrder.LayoutOrder
prl.Padding = UDim.new(0, 6)
prl.Parent = pillRow
local function pill(order, w, label)
local b = Instance.new("TextButton")
b.Text = label; b.TextSize = 11.5
themed(b, "TextColor3", "text")
b.BorderSizePixel = 0; b.Font = FONT_MED
b.Size = UDim2.new(0, w, 0, 26); b.LayoutOrder = order
b.Parent = pillRow; corner(b, 13); stroke(b, "line", 1)
hoverable(b, "pillBg", "pillHover")
return b
end
modelBtn = pill(1, 108, "β Sonnet 4.6")
effortBtn = pill(2, 78, "β Medium")
thinkBtn = pill(3, 68, "β Think")
ctxLabel = text(pillRow, "β§ no ctx", 11, "mute", UDim2.new(0, 74, 0, 26), nil, FONT_MED, Enum.TextXAlignment.Center)
ctxLabel.TextYAlignment = Enum.TextYAlignment.Center
ctxLabel.LayoutOrder = 4
local function paintThink()
if thinkOn then
thinkBtn.BackgroundColor3 = THEME.thinkOnBg
thinkBtn.TextColor3 = THEME.thinkOnText
else
thinkBtn.BackgroundColor3 = THEME.pillBg
thinkBtn.TextColor3 = THEME.text
end
end
thinkBtn.MouseEnter:Connect(paintThink)
thinkBtn.MouseLeave:Connect(paintThink)
themedFn(thinkBtn, "BackgroundColor3", function(t) return thinkOn and t.thinkOnBg or t.pillBg end)
themedFn(thinkBtn, "TextColor3", function(t) return thinkOn and t.thinkOnText or t.text end)
sendBtn = primaryBtn(comp, "β¨ Send", UDim2.new(0, 130, 0, 32), UDim2.new(0.5, -65, 0, 104))
sendGrad = sendBtn:FindFirstChildOfClass("UIGradient")
local sendCorner = sendBtn:FindFirstChildOfClass("UICorner")
if sendCorner then sendCorner.CornerRadius = UDim.new(0, 10) end
local fixBtn = ghostBtn(comp, "Fix errors", UDim2.new(0, 76, 0, 26), UDim2.new(0, 12, 0, 107))
fixBtn.TextSize = 10.5
local attachBtn = ghostBtn(comp, "π", UDim2.new(0, 30, 0, 26), UDim2.new(1, -42, 0, 107))
attachBtn.TextSize = 12
local chipsRow = frame(chatScreen, UDim2.new(1, -24, 0, 26), UDim2.new(0, 12, 1, -(COMP_H + 32)))
chipsRow.Visible = false; chipsRow.ZIndex = 5
local chipsL = Instance.new("UIListLayout")
chipsL.FillDirection = Enum.FillDirection.Horizontal
chipsL.VerticalAlignment = Enum.VerticalAlignment.Center
chipsL.Padding = UDim.new(0, 6)
chipsL.Parent = chipsRow
local hintRow = frame(chatScreen, UDim2.new(1, -24, 0, 20), UDim2.new(0, 12, 1, -(COMP_H + 26)), "menuBg")
hintRow.Visible = false; hintRow.ZIndex = 5
corner(hintRow, 8); stroke(hintRow, "line", 1)
local hintTxt = text(hintRow, SLASH_HINT, 10.5, "dim", UDim2.new(1, -16, 1, 0), UDim2.new(0, 8, 0, 0), FONT_MONO)
hintTxt.TextYAlignment = Enum.TextYAlignment.Center; hintTxt.ZIndex = 6
local function layoutFloaters()
local y = -(COMP_H + 6)
if chipsRow.Visible then
y -= 26
chipsRow.Position = UDim2.new(0, 12, 1, y)
y -= 4
end
if hintRow.Visible then
y -= 20
hintRow.Position = UDim2.new(0, 12, 1, y)
end
end
refreshChips = function()
for _, c in ipairs(chipsRow:GetChildren()) do
if c:IsA("Frame") then c:Destroy() end
end
for i, url in ipairs(pendingImages) do
local chip = frame(chipsRow, UDim2.new(0, 120, 0, 24), nil, "chipBg")
chip.ZIndex = 6; chip.LayoutOrder = i
corner(chip, 12); stroke(chip, "line2", 1)
local lbl = text(chip, "πΌ " .. url:gsub("^https?://", ""), 10.5, "dim", UDim2.new(1, -28, 1, 0), UDim2.new(0, 8, 0, 0), FONT_MED)
lbl.TextYAlignment = Enum.TextYAlignment.Center
lbl.TextWrapped = false; lbl.TextTruncate = Enum.TextTruncate.AtEnd
lbl.ZIndex = 7
local x = Instance.new("TextButton")
x.Size = UDim2.fromOffset(18, 18); x.Position = UDim2.new(1, -22, 0.5, -9)
x.BackgroundTransparency = 1; x.Text = "β"; x.TextSize = 11
themed(x, "TextColor3", "mute")
x.Font = FONT; x.ZIndex = 7; x.Parent = chip
x.MouseEnter:Connect(function() x.TextColor3 = ERR end)
x.MouseLeave:Connect(function() x.TextColor3 = THEME.mute end)
local idx = i
x.MouseButton1Click:Connect(function()
table.remove(pendingImages, idx)
refreshChips()
end)
end
chipsRow.Visible = #pendingImages > 0
layoutFloaters()
end
local function attachPrompt()
overlay.Visible = true
for _, c in ipairs(overlay:GetChildren()) do c:Destroy() end
local back = Instance.new("TextButton")
back.Size = UDim2.new(1, 0, 1, 0); back.BackgroundColor3 = THEME.bg; back.BackgroundTransparency = 0.4
back.Text = ""; back.ZIndex = 50; back.Parent = overlay
back.MouseButton1Click:Connect(function() overlay.Visible = false end)
local dlg = frame(overlay, UDim2.new(0, 280, 0, 124), UDim2.new(0.5, -140, 0.5, -62), THEME.menuBg)
dlg.ZIndex = 55; corner(dlg, 12); stroke(dlg, THEME.line2, 1); pad(dlg, 14, 14, 12, 12)
local t = text(dlg, "π Attach image (URL or asset id)", 13, THEME.text, UDim2.new(1, 0, 0, 20), nil, FONT_BOLD)
t.ZIndex = 56
local box = Instance.new("TextBox")
box.Text = ""; box.TextSize = 13; box.TextColor3 = THEME.text
box.PlaceholderText = "https://β¦ or 123456789"; box.PlaceholderColor3 = THEME.mute
box.BackgroundColor3 = THEME.pillBg; box.BorderSizePixel = 0
box.Font = FONT; box.ClearTextOnFocus = false
box.Size = UDim2.new(1, 0, 0, 30); box.Position = UDim2.new(0, 0, 0, 28)
box.TextXAlignment = Enum.TextXAlignment.Left; box.ZIndex = 56
corner(box, 8); stroke(box, THEME.line, 1); pad(box, 8, 8, 0, 0); box.Parent = dlg
local okB = primaryBtn(dlg, "Add", UDim2.new(1, 0, 0, 28), UDim2.new(0, 0, 0, 68))
okB.ZIndex = 56; okB.TextSize = 12
okB.MouseButton1Click:Connect(function()
local v = box.Text:match("^%s*(.-)%s*$")
overlay.Visible = false
if v == "" then return end
local url = v:match("^%d+$") and ("https://assetdelivery.roblox.com/v1/asset?id=" .. v) or v
if #pendingImages < 3 then
table.insert(pendingImages, url)
refreshChips()
end
end)
box:CaptureFocus()
end
fixBtn.MouseButton1Click:Connect(function()
if active then return end
inputBox.Text = "/fix"
doSend()
end)
attachBtn.MouseButton1Click:Connect(attachPrompt)
inputBox:GetPropertyChangedSignal("Text"):Connect(function()
local show = inputBox.Text == "/"
if hintRow.Visible ~= show then
hintRow.Visible = show
layoutFloaters()
end
end)
local ver = text(comp, VERSION, 10, "mute", UDim2.new(0, 90, 0, 16), UDim2.new(1, -100, 1, -20), FONT_MONO, Enum.TextXAlignment.Right)
ver.TextYAlignment = Enum.TextYAlignment.Center
local function findModel(id)
for _, m in ipairs(MODELS) do if m.id == id then return m end end
return nil
end
local function refreshModelBtn()
local m = findModel(modelId)
modelBtn.Text = "β " .. (m and m.label or modelId)
end
refreshModelBtn()
effortBtn.Text = "β " .. effortId:sub(1, 1):upper() .. effortId:sub(2)
local estLabel = text(comp, "β β¦", 10, "mute", UDim2.new(0, 130, 0, 16), UDim2.new(0, 12, 1, -20), FONT_MONO)
estLabel.TextYAlignment = Enum.TextYAlignment.Center
local ctxChars = 0
local function refreshCtxChars()
ctxChars = 0
for _, inst in ipairs(Selection:Get()) do
if inst:IsA("LuaSourceContainer") then
local src = getSource(inst)
if src then ctxChars += #src end
end
end
end
local function refreshEstimate()
local m = findModel(modelId)
local mult = (m and m.mult) or 1
local inTok = (ctxChars + #inputBox.Text + 600) / 4
local outTok = (EST_OUT_TOKENS[effortId] or 1600) * (thinkOn and 1.4 or 1)
local cr = math.max(0.01, (inTok + outTok * 4) * mult / CREDIT_TOKENS)
estLabel.Text = "β " .. fmtCr(cr) .. " cr"
end
inputBox:GetPropertyChangedSignal("Text"):Connect(refreshEstimate)
refreshCtxChars(); refreshEstimate()
adoptServerModels = function(list)
if type(list) ~= "table" or #list == 0 then return end
local out = {}
for _, m in ipairs(list) do
if type(m) == "table" and m.id and m.label then
table.insert(out, {
id = m.id, label = m.label, vendor = m.vendor or "anthropic",
mult = tonumber(m.mult) or 1,
available = m.available ~= false,
})
end
end
if #out == 0 then return end
MODELS = out
local cur = findModel(modelId)
if not cur or cur.available == false then
for _, m in ipairs(MODELS) do
if m.available ~= false then modelId = m.id; save("model", modelId); break end
end
end
refreshModelBtn()
refreshEstimate()
end
local function openMenu(anchorBtn, items, onPick)
overlay.Visible = true
for _, c in ipairs(overlay:GetChildren()) do c:Destroy() end
local back = Instance.new("TextButton")
back.Size = UDim2.new(1, 0, 1, 0); back.BackgroundTransparency = 1; back.Text = ""
back.ZIndex = 50; back.Parent = overlay
back.MouseButton1Click:Connect(function() overlay.Visible = false end)
local w = 210
local ah = anchorBtn.AbsolutePosition
local rp = root.AbsolutePosition
local relX = math.clamp(ah.X - rp.X, 8, math.max(8, root.AbsoluteSize.X - w - 8))
local relY = ah.Y - rp.Y
local itemH = 32
local wantH = #items * itemH + 12
local maxH = math.min(wantH, relY - 16, 360)
if maxH < itemH + 12 then maxH = math.min(wantH, 360) end
local menu = Instance.new("Frame")
menu.Size = UDim2.new(0, w, 0, maxH)
menu.Position = UDim2.new(0, relX, 0, math.max(8, relY - maxH - 6))
menu.BackgroundColor3 = THEME.menuBg
menu.BorderSizePixel = 0
menu.ZIndex = 51; menu.Parent = overlay
corner(menu, 10); stroke(menu, THEME.line2, 1)
local list = Instance.new("ScrollingFrame")
list.Size = UDim2.new(1, 0, 1, 0); list.BackgroundTransparency = 1; list.BorderSizePixel = 0
list.ScrollBarThickness = 4; list.ScrollBarImageColor3 = THEME.line2
list.CanvasSize = UDim2.new(0, 0, 0, wantH); list.ScrollingDirection = Enum.ScrollingDirection.Y
list.ZIndex = 51; list.Parent = menu
pad(list, 6, 6, 6, 6)
local ml = Instance.new("UIListLayout"); ml.Padding = UDim.new(0, 2); ml.Parent = list
for i, it in ipairs(items) do
local ib = Instance.new("TextButton")
ib.Size = UDim2.new(1, 0, 0, itemH - 2); ib.BackgroundColor3 = THEME.menuHover; ib.BackgroundTransparency = 1
ib.BorderSizePixel = 0; ib.Text = ""; ib.AutoButtonColor = false; ib.LayoutOrder = i
ib.ZIndex = 52; ib.Parent = list; corner(ib, 6)
if it.dotColor then
local dot = frame(ib, UDim2.fromOffset(6, 6), UDim2.new(0, 10, 0.5, -3), it.dotColor)
dot.ZIndex = 53; corner(dot, 3)
if it.disabled then dot.BackgroundTransparency = 0.6 end
end
local rightPad = it.actions and 54 or 24
local lbl = text(ib, it.label, 12.5, it.disabled and THEME.mute or THEME.text,
UDim2.new(1, -(it.dotColor and 22 or 10) - rightPad, 1, 0), UDim2.new(0, it.dotColor and 22 or 10, 0, 0))
lbl.TextYAlignment = Enum.TextYAlignment.Center; lbl.ZIndex = 53
lbl.TextTruncate = Enum.TextTruncate.AtEnd
if not it.disabled then
ib.MouseEnter:Connect(function() ib.BackgroundTransparency = 0.3 end)
ib.MouseLeave:Connect(function() ib.BackgroundTransparency = 1 end)
ib.MouseButton1Click:Connect(function()
overlay.Visible = false
onPick(it.value)
end)
end
if it.actions then
local editB = Instance.new("TextButton")
editB.Size = UDim2.fromOffset(22, 22); editB.Position = UDim2.new(1, -50, 0.5, -11)
editB.BackgroundTransparency = 1; editB.Text = "β"; editB.TextSize = 13
editB.TextColor3 = THEME.mute; editB.Font = FONT; editB.ZIndex = 54; editB.Parent = ib
editB.MouseEnter:Connect(function() editB.TextColor3 = VIOLET end)
editB.MouseLeave:Connect(function() editB.TextColor3 = THEME.mute end)
editB.MouseButton1Click:Connect(function()
overlay.Visible = false
if it.actions.rename then it.actions.rename(it.value, it.label) end
end)
local delB = Instance.new("TextButton")
delB.Size = UDim2.fromOffset(22, 22); delB.Position = UDim2.new(1, -26, 0.5, -11)
delB.BackgroundTransparency = 1; delB.Text = "β"; delB.TextSize = 13
delB.TextColor3 = THEME.mute; delB.Font = FONT; delB.ZIndex = 54; delB.Parent = ib
delB.MouseEnter:Connect(function() delB.TextColor3 = ERR end)
delB.MouseLeave:Connect(function() delB.TextColor3 = THEME.mute end)
delB.MouseButton1Click:Connect(function()
if it.actions.delete then it.actions.delete(it.value, delB) end
end)
end
end
end
modelBtn.MouseButton1Click:Connect(function()
local items = {}
for _, m in ipairs(MODELS) do
local unavailable = m.available == false
table.insert(items, {
label = m.label .. (unavailable and " Β· soon" or ""),
value = m.id,
dotColor = m.vendor == "anthropic" and CORAL or VIOLET,
disabled = unavailable,
})
end
openMenu(modelBtn, items, function(v)
modelId = v; save("model", v); refreshModelBtn(); refreshEstimate()
end)
end)
effortBtn.MouseButton1Click:Connect(function()
local items = {}
for _, e in ipairs(EFFORTS) do
table.insert(items, { label = e:sub(1, 1):upper() .. e:sub(2), value = e })
end
openMenu(effortBtn, items, function(v)
effortId = v; save("effort", v)
effortBtn.Text = "β " .. v:sub(1, 1):upper() .. v:sub(2)
refreshEstimate()
end)
end)
thinkBtn.MouseButton1Click:Connect(function()
thinkOn = not thinkOn; save("think", thinkOn)
paintThink()
refreshEstimate()
end)
gearBtn.MouseButton1Click:Connect(function()
local fsLabel = "Normal"
for _, s in ipairs(FONT_SCALES) do if math.abs(s.v - fontScale) < 0.01 then fsLabel = s.label end end
local items = {
{ label = (autoApply and "β " or " ") .. "β‘ Auto-apply code", value = "auto" },
{ label = (sendOnEnter and "β " or " ") .. "β΅ Enter = send", value = "enter" },
{ label = (compactMode and "β " or " ") .. "β€ Compact chat", value = "compact" },
{ label = " π Font size: " .. fsLabel, value = "font" },
{ label = " π Clear chat history", value = "clearlocal" },
{ label = " πͺ Sign out", value = "signout" },
}
openMenu(gearBtn, items, function(v)
if v == "auto" then
autoApply = not autoApply; save("autoapply", autoApply)
elseif v == "enter" then
sendOnEnter = not sendOnEnter; save("sendenter", sendOnEnter)
elseif v == "compact" then
compactMode = not compactMode; save("compact", compactMode)
if chatId and chatId ~= 0 and loadChat then loadChat(chatId) end
elseif v == "font" then
local idx = 1
for i, s in ipairs(FONT_SCALES) do if math.abs(s.v - fontScale) < 0.01 then idx = i end end
local nxt = FONT_SCALES[(idx % #FONT_SCALES) + 1]
fontScale = nxt.v; save("fontscale", fontScale)
applyFontScale()
elseif v == "clearlocal" then
for _, c in ipairs(scroll:GetChildren()) do if c:IsA("Frame") then c:Destroy() end end
msgCount = 0; chatId = 0; save("chat_id", 0)
refreshQuickActions()
elseif v == "signout" then
token = ""; save("token", ""); chatId = 0; save("chat_id", 0); showPair()
end
end)
end)
newBtn.MouseButton1Click:Connect(function()
task.spawn(function()
for _, c in ipairs(scroll:GetChildren()) do if c:IsA("Frame") then c:Destroy() end end
msgCount = 0; chatId = 0; save("chat_id", 0)
refreshQuickActions()
local d = request("POST", "/api/plugin/new_chat", { model = modelId })
if d and d.chat_id then chatId = d.chat_id; save("chat_id", chatId) end
end)
end)
local function renamePrompt(id, oldTitle)
overlay.Visible = true
for _, c in ipairs(overlay:GetChildren()) do c:Destroy() end
local back = Instance.new("TextButton")
back.Size = UDim2.new(1, 0, 1, 0); back.BackgroundColor3 = THEME.bg; back.BackgroundTransparency = 0.4
back.Text = ""; back.ZIndex = 50; back.Parent = overlay
back.MouseButton1Click:Connect(function() overlay.Visible = false end)
local dlg = frame(overlay, UDim2.new(0, 260, 0, 120), UDim2.new(0.5, -130, 0.5, -60), THEME.menuBg)
dlg.ZIndex = 55; corner(dlg, 12); stroke(dlg, THEME.line2, 1); pad(dlg, 14, 14, 12, 12)
local t = text(dlg, "β Rename chat", 13, THEME.text, UDim2.new(1, 0, 0, 20), nil, FONT_BOLD)
t.ZIndex = 56
local box = Instance.new("TextBox")
box.Text = oldTitle or ""; box.TextSize = 13; box.TextColor3 = THEME.text
box.PlaceholderText = "New title"; box.PlaceholderColor3 = THEME.mute
box.BackgroundColor3 = THEME.pillBg; box.BorderSizePixel = 0
box.Font = FONT; box.ClearTextOnFocus = false
box.Size = UDim2.new(1, 0, 0, 30); box.Position = UDim2.new(0, 0, 0, 26)
box.TextXAlignment = Enum.TextXAlignment.Left; box.ZIndex = 56
corner(box, 8); stroke(box, THEME.line, 1); pad(box, 8, 8, 0, 0); box.Parent = dlg
local okB = primaryBtn(dlg, "Save", UDim2.new(1, 0, 0, 28), UDim2.new(0, 0, 0, 64))
okB.ZIndex = 56; okB.TextSize = 12
okB.MouseButton1Click:Connect(function()
local newT = box.Text:match("^%s*(.-)%s*$")
overlay.Visible = false
if newT ~= "" and newT ~= oldTitle then
task.spawn(function() request("PATCH", "/api/plugin/chats/" .. id, { title = newT }) end)
end
end)
box:CaptureFocus()
end
histBtn.MouseButton1Click:Connect(function()
task.spawn(function()
local chats = request("GET", "/api/plugin/chats")
if not chats then return end
local items = {}
local armedDelete = {}
local actions = {
rename = renamePrompt,
delete = function(id, btn)
if not armedDelete[id] then
armedDelete[id] = true
if btn then btn.Text = "sure?"; btn.TextColor3 = ERR end
task.delay(3, function()
armedDelete[id] = nil
if btn and btn.Parent then btn.Text = "β"; btn.TextColor3 = THEME.mute end
end)
return
end
armedDelete[id] = nil
if btn and btn.Parent then btn.Parent:Destroy() end
task.spawn(function()
request("DELETE", "/api/plugin/chats/" .. id)
if chatId == id then
for _, c in ipairs(scroll:GetChildren()) do if c:IsA("Frame") then c:Destroy() end end
msgCount = 0; chatId = 0; save("chat_id", 0)
refreshQuickActions()
end
end)
end,
}
for _, c in ipairs(chats) do
local title = utf8Trunc(c.title or "Chat", 40)
table.insert(items, { label = title, value = c.id, dotColor = VIOLET, actions = actions })
end
if #items == 0 then items = { { label = "(no chats yet)", value = nil } } end
openMenu(histBtn, items, function(v)
if v and loadChat then loadChat(v) end
end)
end)
end)
local function refreshCtxLabel()
refreshCtxChars(); refreshEstimate()
local n = 0
for _, i in ipairs(Selection:Get()) do if i:IsA("LuaSourceContainer") then n += 1 end end
if n > 0 then
ctxLabel.Text = "β§ " .. n .. " script" .. (n == 1 and "" or "s"); ctxLabel.TextColor3 = VIOLET
elseif activeEditorScript() then
ctxLabel.Text = "β§ open tab"; ctxLabel.TextColor3 = SKY
else
ctxLabel.Text = "β§ no ctx"; ctxLabel.TextColor3 = THEME.mute
end
end
Selection.SelectionChanged:Connect(refreshCtxLabel)
pcall(function() StudioService:GetPropertyChangedSignal("ActiveScript"):Connect(refreshCtxLabel) end)
refreshCtxLabel()
end
local function scrollDown() scroll.CanvasPosition = Vector2.new(0, 1e6) end
local FADE_INFO = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local function fadeIn(bubble, accentBar)
local targetBg = bubble.BackgroundTransparency
bubble.BackgroundTransparency = 1
TweenService:Create(bubble, FADE_INFO, { BackgroundTransparency = targetBg }):Play()
if accentBar then
local targetBar = accentBar.BackgroundTransparency
accentBar.BackgroundTransparency = 1
TweenService:Create(accentBar, FADE_INFO, { BackgroundTransparency = targetBar }):Play()
end
end
local function newBubble(role)
msgCount += 1
refreshQuickActions()
local wrap = Instance.new("Frame")
wrap.BackgroundTransparency = 1; wrap.Size = UDim2.new(1, 0, 0, 0)
wrap.AutomaticSize = Enum.AutomaticSize.Y; wrap.LayoutOrder = msgCount; wrap.Parent = scroll
local wl = Instance.new("UIListLayout"); wl.Padding = UDim.new(0, compactMode and 2 or 4); wl.SortOrder = Enum.SortOrder.LayoutOrder; wl.Parent = wrap
local isUser = role == "user"
if not compactMode then
local rl = text(wrap, isUser and "You" or "Sculpt", 11.5, isUser and CORAL or VIOLET, UDim2.new(1, 0, 0, 16), nil, FONT_BOLD)
rl.LayoutOrder = 1
end
local holder = Instance.new("Frame")
holder.BackgroundTransparency = 1; holder.BorderSizePixel = 0
holder.Size = UDim2.new(1, 0, 0, 0); holder.AutomaticSize = Enum.AutomaticSize.Y
holder.LayoutOrder = 2; holder.Parent = wrap
local bar = frame(holder, UDim2.new(0, 2, 1, -6), UDim2.new(0, 0, 0, 3), isUser and CORAL or VIOLET)
bar.BackgroundTransparency = 0.25
corner(bar, 1)
local bubble = Instance.new("Frame")
themed(bubble, "BackgroundColor3", isUser and "userBubble" or "panel")
bubble.BorderSizePixel = 0
bubble.Size = UDim2.new(1, -8, 0, 0); bubble.Position = UDim2.new(0, 8, 0, 0)
bubble.AutomaticSize = Enum.AutomaticSize.Y
bubble.Parent = holder
corner(bubble, 10); stroke(bubble, "line", 1, 0.75); pad(bubble, compactMode and 8 or 12)
fadeIn(bubble, bar)
return bubble
end
local ANNOT = {
BUG = ERR,
RACE = PEACH,
WARN = PEACH,
PERF = SKY,
SUGGESTION = VIOLET,
FIX = LIME,
NOTE = Color3.fromRGB(162, 156, 184),
}
local function escapeRich(s)
return s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">")
end
local function richify(s)
local out = escapeRich(s)
out = out:gsub("`([^`\n]+)`", function(m)
return ' ' .. m .. ' '
end)
out = out:gsub("%*%*([^%*\n]+)%*%*", "%1")
return out
end
local IMG_EXT = { png = true, jpg = true, jpeg = true, gif = true, webp = true }
local imageCache = {}
local function findImageUrls(str)
local urls, seen = {}, {}
for url in str:gmatch("https?://[%w%-%._~:/%?#%[%]@!%$&'%(%)%*%+,;=%%]+") do
local clean = url:gsub("[%.,%);'\"]+$", "")
local ext = clean:match("%.(%w+)$") or clean:match("%.(%w+)%?")
local isAsset = clean:find("assetdelivery%.roblox%.com") ~= nil
if ((ext and IMG_EXT[ext:lower()]) or isAsset) and not seen[clean] then
seen[clean] = true
table.insert(urls, clean)
end
end
return urls
end
local function fetchEditableImage(url)
local cached = imageCache[url]
if cached then
if cached.failed then return nil end
return cached
end
local ok, result = pcall(function()
local d, err = request("POST", "/api/plugin/image", { url = url, max_size = 320 })
if not d or not d.rgba_base64 or not d.width or not d.height then
error(err or "no image data")
end
local w, h = d.width, d.height
local buf = b64ToBuffer(d.rgba_base64, w * h * 4)
if not buf then error("bad pixel payload") end
local editable = AssetService:CreateEditableImage({ Size = Vector2.new(w, h) })
editable:WritePixelsBuffer(Vector2.zero, Vector2.new(w, h), buf)
return { editable = editable, w = w, h = h }
end)
if ok and result then
imageCache[url] = result
return result
end
imageCache[url] = { failed = true }
return nil
end
local function addImagePreview(bubble, url, order)
local card = frame(bubble, UDim2.new(1, 0, 0, 40), nil, "chipBg")
card.LayoutOrder = order
card.ClipsDescendants = true
corner(card, 10); stroke(card, "line", 1)
local loading = text(card, "πΌ loading imageβ¦", 11.5, "mute", UDim2.new(1, -20, 1, 0), UDim2.new(0, 10, 0, 0), FONT)
loading.TextYAlignment = Enum.TextYAlignment.Center
task.spawn(function()
local img = fetchEditableImage(url)
if not card.Parent then return end
if not img then
loading.Text = "πΌ image unavailable"
card.Size = UDim2.new(0, 160, 0, 26)
return
end
local maxH = 180
local dispH = math.min(maxH, img.h)
local dispW = math.floor(img.w * dispH / img.h + 0.5)
local label = Instance.new("ImageLabel")
label.Size = UDim2.new(1, 0, 1, 0)
label.BackgroundTransparency = 1
label.ScaleType = Enum.ScaleType.Fit
local okSet = pcall(function()
label.ImageContent = Content.fromObject(img.editable)
end)
if okSet then
loading:Destroy()
card.Size = UDim2.new(0, dispW, 0, dispH)
label.Parent = card
corner(label, 10)
else
imageCache[url] = { failed = true }
label:Destroy()
loading.Text = "πΌ image unavailable"
card.Size = UDim2.new(0, 160, 0, 26)
end
end)
end
local function renderInto(bubble, str)
local bl = bubble:FindFirstChildOfClass("UIListLayout")
if not bl then
bl = Instance.new("UIListLayout"); bl.Padding = UDim.new(0, 8); bl.SortOrder = Enum.SortOrder.LayoutOrder; bl.Parent = bubble
end
local order = 0
local function addAnnotCard(cat, lineNo, msg)
order += 1
local accent = ANNOT[cat]
local card = Instance.new("Frame")
card.BorderSizePixel = 0
themedFn(card, "BackgroundColor3", function(t)
return t.bg2:Lerp(accent, t.annotMix)
end)
card.Size = UDim2.new(1, 0, 0, 0)
card.AutomaticSize = Enum.AutomaticSize.Y; card.LayoutOrder = order
card.Parent = bubble
corner(card, 8)
local st = stroke(card, accent, 1); st.Transparency = 0.6
pad(card, 10, 10, 8, 8)
local head = cat .. (lineNo and (" Β· LINE " .. lineNo) or "")
text(card, "β " .. head, 10.5, accent, UDim2.new(1, 0, 0, 15), nil, FONT_MONO)
local body = text(card, "", 13.5, "text", UDim2.new(1, 0, 0, 0), UDim2.new(0, 0, 0, 18))
body.RichText = true
body.Text = richify(msg)
body.AutomaticSize = Enum.AutomaticSize.Y
end
local function addPlain(chunk)
chunk = chunk:match("^%s*(.-)%s*$")
if chunk == "" then return end
local buf = {}
local function flushBuf()
if #buf == 0 then return end
local t = table.concat(buf, "\n"):match("^%s*(.-)%s*$")
buf = {}
if t == "" then return end
order += 1
local l = text(bubble, "", 14, "text", UDim2.new(1, 0, 0, 0))
l.RichText = true
l.Text = richify(t)
l.AutomaticSize = Enum.AutomaticSize.Y; l.LayoutOrder = order
end
for line in (chunk .. "\n"):gmatch("(.-)\n") do
local cat, rest = line:match("^%s*[ββ’]%s*(%u+)%s*[β%-β:]+%s*(.+)$")
if cat and ANNOT[cat] then
flushBuf()
local lineNo, msg = rest:match("^LINE%s+(%d+)%s*[:β%-β]+%s*(.+)$")
addAnnotCard(cat, lineNo, msg or rest)
else
table.insert(buf, line)
end
end
flushBuf()
end
local pos = 1
while true do
local s, e, _, code = str:find("```(%w*)%s*\n(.-)```", pos)
addPlain(str:sub(pos, s and s - 1 or #str))
if not s then break end
order += 1
local card = frame(bubble, UDim2.new(1, 0, 0, 0), nil, "codeBg")
card.AutomaticSize = Enum.AutomaticSize.Y; card.LayoutOrder = order
corner(card, 8); stroke(card, "line", 1); pad(card, 12, 12, 10, 10)
local cl = text(card, code:match("^(.-)%s*$"), 13, "codeText", UDim2.new(1, 0, 0, 0), nil, FONT_MONO)
cl.AutomaticSize = Enum.AutomaticSize.Y
pos = e + 1
end
for _, url in ipairs(findImageUrls(str)) do
order += 1
addImagePreview(bubble, url, order)
end
end
local function addFooter(bubble, modelLabel, elapsed, runs, fellBack)
local sec = math.floor(elapsed + 0.5)
local tstr = sec < 60 and (sec .. "s") or string.format("%dm%02ds", math.floor(sec / 60), sec % 60)
local f = text(bubble, "β " .. modelLabel .. " Β· " .. tstr .. " Β· " .. fmtCr(runs) .. " cr" .. (fellBack and " Β· β― fallback" or ""),
11, "mute", UDim2.new(1, 0, 0, 16), nil, FONT_MONO)
f.LayoutOrder = 500
end
local function addDiff(bubble, ins, del, name)
local chip = frame(bubble, UDim2.new(1, 0, 0, 22), nil, "chipBg")
chip.LayoutOrder = 400; corner(chip, 6); pad(chip, 10, 10, 0, 0)
local t = text(chip, "β " .. name .. " +" .. ins .. " -" .. del, 11, GREEN, UDim2.new(1, 0, 1, 0), nil, FONT_MONO)
t.TextYAlignment = Enum.TextYAlignment.Center
end
local applyOrder = 0
local function openCopyModal(code)
overlay.Visible = true
for _, c in ipairs(overlay:GetChildren()) do c:Destroy() end
local back = Instance.new("TextButton")
back.Size = UDim2.new(1, 0, 1, 0); back.BackgroundColor3 = THEME.bg; back.BackgroundTransparency = 0.4
back.Text = ""; back.ZIndex = 50; back.Parent = overlay
back.MouseButton1Click:Connect(function() overlay.Visible = false end)
local dlg = frame(overlay, UDim2.new(1, -48, 0, 300), UDim2.new(0, 24, 0.5, -150), THEME.menuBg)
dlg.ZIndex = 55; corner(dlg, 12); stroke(dlg, THEME.line2, 1); pad(dlg, 14, 14, 12, 12)
local t = text(dlg, "Select all and Ctrl+C", 13, THEME.text, UDim2.new(1, 0, 0, 20), nil, FONT_BOLD)
t.ZIndex = 56
local boxWrap = frame(dlg, UDim2.new(1, 0, 1, -66), UDim2.new(0, 0, 0, 26), THEME.codeBg)
boxWrap.ZIndex = 56; corner(boxWrap, 8); stroke(boxWrap, THEME.line, 1)
local box = Instance.new("TextBox")
box.Text = code; box.TextSize = 12
box.TextColor3 = THEME.codeText
box.BackgroundTransparency = 1; box.BorderSizePixel = 0
box.Font = FONT_MONO; box.ClearTextOnFocus = false; box.TextEditable = false
box.MultiLine = true; box.TextWrapped = false
box.TextXAlignment = Enum.TextXAlignment.Left; box.TextYAlignment = Enum.TextYAlignment.Top
box.Size = UDim2.new(1, -16, 1, -12); box.Position = UDim2.new(0, 8, 0, 6)
box.ClipsDescendants = true
box.ZIndex = 57; box.Parent = boxWrap
local closeB = primaryBtn(dlg, "Close", UDim2.new(0, 100, 0, 28), UDim2.new(0.5, -50, 1, -32))
closeB.ZIndex = 56; closeB.TextSize = 12
closeB.MouseButton1Click:Connect(function() overlay.Visible = false end)
box:CaptureFocus()
end
local function addCopyBtn(row, code, x)
local b = Instance.new("TextButton")
b.Text = "β§ Copy"; b.TextSize = 11
themed(b, "TextColor3", "text")
b.BorderSizePixel = 0; b.Font = FONT_MED
b.Size = UDim2.new(0, 66, 0, 26); b.Position = UDim2.new(0, x, 0, 0)
b.Parent = row; corner(b, 13); stroke(b, "line2", 1)
hoverable(b, "ghostBg", "ghostHover")
b.MouseButton1Click:Connect(function() openCopyModal(code) end)
end
local function openDiffPreview(target, code, onConfirm)
overlay.Visible = true
for _, c in ipairs(overlay:GetChildren()) do c:Destroy() end
local back = Instance.new("TextButton")
back.Size = UDim2.new(1, 0, 1, 0); back.BackgroundColor3 = THEME.bg; back.BackgroundTransparency = 0.4
back.Text = ""; back.ZIndex = 50; back.Parent = overlay
back.MouseButton1Click:Connect(function() overlay.Visible = false end)
local before = getSource(target) or ""
local ins, del = diffStats(before, code)
local dlg = frame(overlay, UDim2.new(1, -48, 0, 340), UDim2.new(0, 24, 0.5, -170), THEME.menuBg)
dlg.ZIndex = 55; corner(dlg, 12); stroke(dlg, THEME.line2, 1); pad(dlg, 14, 14, 12, 12)
local t = text(dlg, "Apply to " .. target.Name .. "?", 13, THEME.text, UDim2.new(1, 0, 0, 20), nil, FONT_BOLD)
t.ZIndex = 56
local stat = text(dlg, "+" .. ins .. " added -" .. del .. " removed Β· overwrites whole script", 11, ins >= del and GREEN or PEACH, UDim2.new(1, 0, 0, 16), UDim2.new(0, 0, 0, 22), FONT_MONO)
stat.ZIndex = 56
local boxWrap = frame(dlg, UDim2.new(1, 0, 1, -96), UDim2.new(0, 0, 0, 44), THEME.codeBg)
boxWrap.ZIndex = 56; corner(boxWrap, 8); stroke(boxWrap, THEME.line, 1)
local box = Instance.new("TextBox")
box.Text = code; box.TextSize = 12
box.TextColor3 = THEME.codeText
box.BackgroundTransparency = 1; box.BorderSizePixel = 0
box.Font = FONT_MONO; box.ClearTextOnFocus = false; box.TextEditable = false
box.MultiLine = true; box.TextWrapped = false
box.TextXAlignment = Enum.TextXAlignment.Left; box.TextYAlignment = Enum.TextYAlignment.Top
box.Size = UDim2.new(1, -16, 1, -12); box.Position = UDim2.new(0, 8, 0, 6)
box.ClipsDescendants = true; box.ZIndex = 57; box.Parent = boxWrap
local cancelB = textButton(dlg, "Cancel", UDim2.new(0, 96, 0, 30), UDim2.new(0, 0, 1, -32), THEME.ghostBg, THEME.text)
cancelB.ZIndex = 56; cancelB.TextSize = 12; corner(cancelB, 8)
cancelB.MouseButton1Click:Connect(function() overlay.Visible = false end)
local applyB = primaryBtn(dlg, "β‘ Apply", UDim2.new(0, 120, 0, 30), UDim2.new(1, -120, 1, -32))
applyB.ZIndex = 56; applyB.TextSize = 12
applyB.MouseButton1Click:Connect(function() overlay.Visible = false; onConfirm(ins, del) end)
end
local function applyToScript(bubble, code, target)
local function commit(btn, ins, del)
local rec = ChangeHistoryService:TryBeginRecording("Sculpt edit " .. target.Name)
local ok = setSource(target, code)
if rec then
ChangeHistoryService:FinishRecording(rec, ok and Enum.FinishRecordingOperation.Commit or Enum.FinishRecordingOperation.Cancel)
end
if ok then
if not ins then ins, del = diffStats("", code) end
addDiff(bubble, ins, del, target.Name); Selection:Set({ target })
if btn then btn.Text = "β Applied"; btn.BackgroundColor3 = GREEN; btn.TextColor3 = INK end
elseif btn then
btn.Text = "β failed"; btn.BackgroundColor3 = ERR
end
end
if autoApply then
commit(nil)
else
applyOrder += 1
local row = frame(bubble, UDim2.new(1, 0, 0, 28))
row.LayoutOrder = 350 + applyOrder
local b = textButton(row, "β‘ Review & Apply β " .. target.Name, UDim2.new(0, 220, 0, 26), UDim2.new(0, 0, 0, 0), VIOLET)
b.TextSize = 11; corner(b, 13)
b.MouseButton1Click:Connect(function()
openDiffPreview(target, code, function(ins, del) commit(b, ins, del) end)
end)
addCopyBtn(row, code, 226)
end
end
local function createNewScript(bubble, block)
applyOrder += 1
local row = frame(bubble, UDim2.new(1, 0, 0, 28))
row.LayoutOrder = 350 + applyOrder
local b = textButton(row, "οΌ Create " .. block.newName, UDim2.new(0, 190, 0, 26), UDim2.new(0, 0, 0, 0), SKY, INK)
b.TextSize = 11; b.Font = FONT_BOLD; corner(b, 13)
addCopyBtn(row, block.code, 196)
b.MouseButton1Click:Connect(function()
b.Text = "Creatingβ¦"
local className = VALID_SCRIPT_CLASSES[block.newClass] and block.newClass or "Script"
local parentInst = resolveParent(block.newParent or "") or game:GetService("ServerScriptService")
local rec = ChangeHistoryService:TryBeginRecording("Sculpt create " .. block.newName)
local ok, err = pcall(function()
local inst = Instance.new(className)
inst.Name = block.newName
inst.Source = block.code
inst.Parent = parentInst
Selection:Set({ inst })
end)
if rec then
ChangeHistoryService:FinishRecording(rec, ok and Enum.FinishRecordingOperation.Commit or Enum.FinishRecordingOperation.Cancel)
end
if ok then
b.Text = "β Created in " .. parentInst.Name
b.BackgroundColor3 = GREEN
local lines = select(2, block.code:gsub("\n", "")) + 1
addDiff(bubble, lines, 0, block.newName)
else
b.Text = "β " .. tostring(err):sub(1, 24)
b.BackgroundColor3 = ERR; b.TextColor3 = WHITE
end
end)
end
local function wireCodeOps(bubble, blocks, targets)
local byName = {}
for _, t in ipairs(targets) do byName[t.Name] = t end
local unnamedIdx = 0
for _, block in ipairs(blocks) do
if block.newName then
createNewScript(bubble, block)
elseif block.target and byName[block.target] then
applyToScript(bubble, block.code, byName[block.target])
elseif not block.target then
unnamedIdx += 1
if targets[unnamedIdx] then
applyToScript(bubble, block.code, targets[unnamedIdx])
end
end
end
end
loadChat = function(id)
task.spawn(function()
local d = request("GET", "/api/plugin/chats/" .. id)
if not d or not d.messages then return end
for _, c in ipairs(scroll:GetChildren()) do if c:IsA("Frame") then c:Destroy() end end
msgCount = 0
chatId = id; save("chat_id", id)
if d.chat and d.chat.model then
for _, m in ipairs(MODELS) do
if m.id == d.chat.model then
modelId = m.id; save("model", modelId)
modelBtn.Text = "β " .. m.label
break
end
end
end
for _, msg in ipairs(d.messages) do
local b = newBubble(msg.role == "user" and "user" or "assistant")
renderInto(b, msg.content or "")
end
refreshQuickActions()
task.defer(scrollDown)
end)
end
local function modelLabelFor(id)
id = (id or ""):gsub("%-fast$", "")
for _, m in ipairs(MODELS) do if m.id == id then return m.label end end
return id
end
local function fmtSec(s)
s = math.floor(s + 0.5)
return s < 60 and (s .. "s") or string.format("%dm%02ds", math.floor(s / 60), s % 60)
end
local function setSendIdle()
sendBtn.Text = "β¨ Send"; sendBtn.BackgroundColor3 = WHITE
sendBtn.TextColor3 = BTN_TEXT
if sendGrad then sendGrad.Enabled = true end
end
local function setSendBusy()
sendBtn.Text = "βΉ Stop"; sendBtn.BackgroundColor3 = Color3.fromRGB(180, 60, 70)
sendBtn.TextColor3 = WHITE
if sendGrad then sendGrad.Enabled = false end
end
local function chatNote(msg)
local ab = newBubble("assistant")
renderInto(ab, msg)
scrollDown()
end
doSend = function()
if active then
active.cancelled = true
local jid = active.jobId
if jid then
task.spawn(function() request("POST", "/api/plugin/job/" .. jid .. "/cancel") end)
end
if active.body then
active.body.Text = "βΉ Stopped after " .. fmtSec(os.clock() - active.t0) .. "."
active.body.TextColor3 = THEME.mute
end
active = nil; setSendIdle()
return
end
local str = inputBox.Text:match("^%s*(.-)%s*$")
local cmd = str:match("^/(%w+)%s*$")
if cmd then
if cmd == "fix" then
local errLines = collectOutputErrors()
if not errLines then
inputBox.Text = ""
chatNote("No recent errors in Output.")
return
end
str = "Fix these errors from Output:\n" .. errLines
elseif SLASH_PROMPTS[cmd] then
str = SLASH_PROMPTS[cmd]
end
end
if #pendingImages > 0 and str ~= "" then
str = str .. "\n\nAttached images:\n" .. table.concat(pendingImages, "\n")
end
if str == "" then return end
local question = str
local ctx, targets = collectContext()
local fullMsg = question
if ctx then fullMsg = ctx .. "\n\n---\n\nUSER: " .. question end
inputBox.Text = ""
if #pendingImages > 0 then
pendingImages = {}
if refreshChips then refreshChips() end
end
setSendBusy()
local tag = ""
if #targets > 0 then tag = " [" .. #targets .. " script" .. (#targets == 1 and "" or "s") .. "]" end
local ub = newBubble("user"); renderInto(ub, str .. tag)
local ab = newBubble("assistant")
local body = text(ab, "β thinkingβ¦ 0s", 14, "dim", UDim2.new(1, 0, 0, 0))
body.AutomaticSize = Enum.AutomaticSize.Y
scrollDown()
local t0 = os.clock()
active = { cancelled = false, question = question, body = body, t0 = t0, jobId = nil }
local me = active
task.spawn(function()
local frames = { "β", "β", "β", "β" }
local i = 0
while active == me and not me.cancelled do
i += 1
if me.body then me.body.Text = frames[(i % 4) + 1] .. " thinking⦠" .. fmtSec(os.clock() - t0) end
task.wait(0.3)
end
end)
task.spawn(function()
local started, err = request("POST", "/api/plugin/chat", {
content = fullMsg, model = modelId, chat_id = chatId ~= 0 and chatId or nil,
effort = effortId, thinking = thinkOn,
})
if me.cancelled then return end
if not started or not started.job_id then
active = nil; setSendIdle()
body.Text = "β " .. (err or "no response"); body.TextColor3 = ERR
if err and tostring(err):find("401") then task.wait(1); token = ""; save("token", ""); showPair() end
return
end
me.jobId = started.job_id
if started.chat_id and started.chat_id ~= chatId then chatId = started.chat_id; save("chat_id", chatId) end
local data, jerr
while true do
task.wait(2)
if me.cancelled or active ~= me then return end
local d, e = request("GET", "/api/plugin/job/" .. started.job_id)
if d and d.status == "running" then
elseif d and d.status == "done" then
data = d; break
else
jerr = (d and d.error) or e or "job failed"; break
end
end
if me.cancelled or active ~= me then return end
active = nil; setSendIdle()
local elapsed = os.clock() - t0
if not data then
body.Text = "β " .. tostring(jerr); body.TextColor3 = ERR
return
end
body:Destroy()
if data.remaining then runsLabel.Text = fmtCr(data.remaining) .. " cr" end
renderInto(ab, data.text or "(empty)")
addFooter(ab, modelLabelFor(data.model), elapsed, data.spent or 0, data.fell_back)
local blocks = extractCode(data.text or "")
if #blocks > 0 then wireCodeOps(ab, blocks, targets) end
scrollDown()
end)
end
sendBtn.MouseButton1Click:Connect(doSend)
refreshMe = function()
task.spawn(function()
local d = request("GET", "/api/plugin/me")
if not d then return end
if d.runs then runsLabel.Text = fmtCr(d.runs) .. " cr" end
if d.models and adoptServerModels then adoptServerModels(d.models) end
end)
end
task.spawn(function()
local d = request("GET", "/api/plugin/logo")
if not d or not d.rgba_base64 or not d.width or not d.height then return end
local buf = b64ToBuffer(d.rgba_base64, d.width * d.height * 4)
if not buf then return end
local ok, editable = pcall(function()
local e = AssetService:CreateEditableImage({ Size = Vector2.new(d.width, d.height) })
e:WritePixelsBuffer(Vector2.zero, Vector2.new(d.width, d.height), buf)
return e
end)
if not ok or not editable then return end
for _, m in ipairs(logoMarks) do
if m.Parent then
pcall(function()
local g = m:FindFirstChildOfClass("UIGradient")
if g then g:Destroy() end
local img = Instance.new("ImageLabel")
img.Size = UDim2.new(1, 0, 1, 0)
img.BackgroundTransparency = 1
img.ScaleType = Enum.ScaleType.Crop
img.ImageContent = Content.fromObject(editable)
img.ZIndex = m.ZIndex + 1
img.Parent = m
corner(img, math.floor(math.max(m.Size.X.Offset, 12) * 0.27))
m.BackgroundColor3 = Color3.fromRGB(10, 20, 16)
end)
end
end
end)
if token ~= "" then
showChat(); refreshMe()
if chatId and chatId ~= 0 then
loadChat(chatId)
else
welcomeShown = true
local ab = newBubble("assistant")
renderInto(ab, "π Welcome to Sculpt! Select a Script β I read it and can write changes straight in. Try `/fix` after an error, or paste an image URL as a visual reference. β¨")
end
else
showPair()
end
refreshQuickActions()
print("[Sculpt] v3.1 loaded β " .. (token ~= "" and "paired" or "not paired"))