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