-- Made By AI -- local Players = game:GetService("Players") local UserInputService = game:GetService("UserInputService") local TweenService = game:GetService("TweenService") local RunService = game:GetService("RunService") local LogService = game:GetService("LogService") local CaptureService = game:GetService("CaptureService") local AssetService = game:GetService("AssetService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local HttpService = game:GetService("HttpService") local MarketplaceService = game:GetService("MarketplaceService") local player = Players.LocalPlayer local playerGui = player:WaitForChild("PlayerGui") local camera = workspace.CurrentCamera SERIF = Enum.Font.GothamBold pcall(function() SERIF = Enum.Font.Merriweather end) local C = { bg = Color3.fromRGB(24, 23, 21), sidebar = Color3.fromRGB(19, 18, 17), panel = Color3.fromRGB(33, 32, 30), hover = Color3.fromRGB(44, 42, 39), userBubble = Color3.fromRGB(36, 34, 31), code = Color3.fromRGB(16, 15, 14), stroke = Color3.fromRGB(56, 53, 49), text = Color3.fromRGB(247, 245, 240), subtext = Color3.fromRGB(156, 151, 143), error = Color3.fromRGB(235, 110, 110), } local MODELS = { { brand = "deepseek", brandName = "DeepSeek", name = "DeepSeek V4 Flash", short = "V4 Flash", id = "deepseek-v4.1-flash-free", format = "openai", provider = "opencode", icon = "rbxassetid://101905873334212", accent = Color3.fromRGB(77, 107, 254), desc = "Free · OpenCode Zen", think = "DeepThink", vision = true }, { brand = "gpt", brandName = "ChatGPT", name = "GPT-6.1 Sol", short = "6.1 Sol", id = "gpt-6.1-sol", format = "openai", provider = "agentrouter", endpoint = "chat", wire = "chat", icon = "rbxassetid://117321183753096", accent = Color3.fromRGB(16, 163, 127), desc = "Reasoning model", think = "Think", vision = true }, { brand = "gpt", brandName = "ChatGPT", name = "GPT-6 Astra", short = "6 Astra", id = "gpt-6-astra", format = "openai", provider = "agentrouter", endpoint = "chat", wire = "chat", icon = "rbxassetid://117321183753096", accent = Color3.fromRGB(130, 105, 240), desc = "Advanced reasoning", think = "Think", vision = true }, { brand = "opencode", brandName = "GLM", name = "GLM 5.3 Flash Free", short = "GLM Free", id = "glm-5.3-flash-free", format = "openai", provider = "opencode", mark = "glm", accent = Color3.fromRGB(70, 150, 220), desc = "Free · balanced", think = "Think", vision = false }, { brand = "opencode", brandName = "MiMo", name = "MiMo V2.6 Flash Free", short = "MiMo Free", id = "mimo-v2.6-flash-free", format = "openai", provider = "opencode", mark = "mm", accent = Color3.fromRGB(50, 190, 130), desc = "Free · multimodal", think = "Think", vision = true }, { brand = "opencode", brandName = "Big Pickle", name = "Big Pickle", short = "Pickle", id = "big-pickle", format = "openai", provider = "opencode", mark = "bp", accent = Color3.fromRGB(210, 150, 50), desc = "Free · general", think = "Think", vision = false }, { brand = "opencode", brandName = "Nemotron", name = "Nemotron 3.5 Lightning Free", short = "Nemo Free", id = "nemotron-3.5-lightning-free", format = "openai", provider = "opencode", mark = "nv", accent = Color3.fromRGB(110, 80, 230), desc = "Free · long context", think = "Think", vision = false }, { brand = "opencode", brandName = "Beam", name = "Beam Free", short = "Beam Free", id = "beam-free", format = "openai", provider = "opencode", mark = "bm", accent = Color3.fromRGB(40, 170, 210), desc = "Free · reasoning", think = "Think", vision = false }, { brand = "opencode", brandName = "Flint", name = "Flint 40B Free", short = "Flint Free", id = "flint-40b-free", format = "openai", provider = "opencode", mark = "fl", accent = Color3.fromRGB(200, 90, 140), desc = "Free · general", think = "Think", vision = false }, { brand = "opencode", brandName = "Nemotron VL", name = "Nemotron Super VL Free", short = "VL Free", id = "nemotron-3.5-super-vl-free", format = "openai", provider = "opencode", mark = "vl", accent = Color3.fromRGB(230, 90, 70), desc = "Free · vision", think = "Think", vision = true }, } local function modelById(id) if not id then return nil end for _, m in ipairs(MODELS) do if m.id == id then return m end end return nil end function providerLabel(m) local p = m and m.provider if p == "openrouter" then return "OpenRouter" end if p == "opencode" then return "OpenCode Zen" end return "Agent Router" end local function new(class, props, parent) local inst = Instance.new(class) for k, v in pairs(props) do inst[k] = v end inst.Parent = parent return inst end local function round(p, r) return new("UICorner", { CornerRadius = UDim.new(0, r) }, p) end local function stroke(p, color, thickness) return new("UIStroke", { Color = color or C.stroke, Thickness = thickness or 1, ApplyStrokeMode = Enum.ApplyStrokeMode.Border }, p) end local function pad(p, t, r, b, l) return new("UIPadding", { PaddingTop = UDim.new(0, t), PaddingRight = UDim.new(0, r), PaddingBottom = UDim.new(0, b), PaddingLeft = UDim.new(0, l) }, p) end local MOTION = 1 local function tween(inst, t, props) local dur = t * MOTION if dur <= 0 then for k, v in pairs(props) do inst[k] = v end return end TweenService:Create(inst, TweenInfo.new(dur, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), props):Play() end local function text(props, parent) local base = { BackgroundTransparency = 1, Font = Enum.Font.Gotham, TextSize = 14, TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, TextWrapped = true, } for k, v in pairs(props) do base[k] = v end return new("TextLabel", base, parent) end local function button(props, parent) local base = { BackgroundTransparency = 1, AutoButtonColor = false, Text = "", Font = Enum.Font.GothamMedium, TextSize = 14, TextColor3 = C.text, } for k, v in pairs(props) do base[k] = v end return new("TextButton", base, parent) end local function hover(btn, overColor, overTransparency) local key for k, v in pairs(C) do if v == overColor then key = k break end end local c0, t0, hovering = btn.BackgroundColor3, btn.BackgroundTransparency, false btn.MouseEnter:Connect(function() if hovering then return end hovering = true c0, t0 = btn.BackgroundColor3, btn.BackgroundTransparency tween(btn, 0.12, { BackgroundColor3 = key and C[key] or overColor, BackgroundTransparency = overTransparency or 0 }) end) btn.MouseLeave:Connect(function() if not hovering then return end hovering = false tween(btn, 0.12, { BackgroundColor3 = c0, BackgroundTransparency = t0 }) end) end -- Executors expose the clipboard under different names. function copyToClipboard(value) local text = tostring(value or "") local fn = setclipboard or toclipboard or writeclipboard if type(fn) == "function" then pcall(fn, text) return true end return false end local function bar(parent, w, h, x, y, rot, color) local f = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, x, 0.5, y), Size = UDim2.fromOffset(w, h), Rotation = rot, BackgroundColor3 = color, BorderSizePixel = 0, }, parent) round(f, math.min(w, h) / 2) return f end local function icon(kind, parent, color, size) size = size or 20 local s = size / 20 local root = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(size, size), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), }, parent) if kind == "arrowUp" then bar(root, 2.4 * s, 11 * s, 0, 0.5 * s, 0, color) bar(root, 2.4 * s, 6.5 * s, -2.2 * s, -3.6 * s, 45, color) bar(root, 2.4 * s, 6.5 * s, 2.2 * s, -3.6 * s, -45, color) elseif kind == "cross" then bar(root, 2 * s, 15 * s, 0, 0, 45, color) bar(root, 2 * s, 15 * s, 0, 0, -45, color) elseif kind == "plus" then bar(root, 2 * s, 13 * s, 0, 0, 0, color) bar(root, 13 * s, 2 * s, 0, 0, 0, color) elseif kind == "minus" then bar(root, 13 * s, 2 * s, 0, 0, 0, color) elseif kind == "chevron" then bar(root, 2 * s, 7 * s, -2.4 * s, 0, -45, color) bar(root, 2 * s, 7 * s, 2.4 * s, 0, 45, color) elseif kind == "check" then bar(root, 2 * s, 5 * s, -3.5 * s, 1.5 * s, -45, color) bar(root, 2 * s, 10 * s, 1.5 * s, -0.5 * s, 45, color) elseif kind == "menu" then bar(root, 14 * s, 2 * s, 0, -5 * s, 0, color) bar(root, 14 * s, 2 * s, 0, 0, 0, color) bar(root, 14 * s, 2 * s, 0, 5 * s, 0, color) elseif kind == "image" then local f = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(15 * s, 12 * s), BackgroundTransparency = 1 }, root) round(f, 3) stroke(f, color, 1.6) bar(root, 3.6 * s, 3.6 * s, 3 * s, -2 * s, 0, color) bar(root, 9 * s, 1.6 * s, -1.5 * s, 3 * s, -30, color) elseif kind == "terminal" then local f = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(16 * s, 13 * s), BackgroundTransparency = 1 }, root) round(f, 3) stroke(f, color, 1.6) bar(root, 1.6 * s, 4.2 * s, -3.6 * s, -1.4 * s, -45, color) bar(root, 1.6 * s, 4.2 * s, -3.6 * s, 1.4 * s, 45, color) bar(root, 4.5 * s, 1.6 * s, 2.5 * s, 2.5 * s, 0, color) elseif kind == "globe" then local f = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(15 * s, 15 * s), BackgroundTransparency = 1 }, root) round(f, 99) stroke(f, color, 1.6) bar(root, 15 * s, 1.5 * s, 0, 0, 0, color) local e = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(7 * s, 15 * s), BackgroundTransparency = 1 }, root) round(e, 99) stroke(e, color, 1.5) elseif kind == "settings" then local gear = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(12 * s, 12 * s), BackgroundTransparency = 1 }, root) round(gear, 99) stroke(gear, color, 1.6) local center = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(4 * s, 4 * s), BackgroundColor3 = color }, root) round(center, 99) for i = 0, 3 do bar(root, 2 * s, 5 * s, 0, 0, i * 45, color) end elseif kind == "stop" then local f = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(10 * s, 10 * s), BackgroundColor3 = color, BorderSizePixel = 0, }, root) round(f, 2) elseif kind == "file" then local f = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(12 * s, 15 * s), BackgroundTransparency = 1 }, root) round(f, 2) stroke(f, color, 1.5) bar(root, 6 * s, 1.5 * s, -1.5 * s, -2 * s, 0, color) bar(root, 8 * s, 1.5 * s, -1 * s, 2 * s, 0, color) elseif kind == "folder" then local f = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(15 * s, 12 * s), BackgroundTransparency = 1 }, root) round(f, 2) stroke(f, color, 1.5) bar(root, 6 * s, 2 * s, -3 * s, -5 * s, 0, color) elseif kind == "search" then local ring = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, -1 * s, 0.5, -1 * s), Size = UDim2.fromOffset(11 * s, 11 * s), BackgroundTransparency = 1 }, root) round(ring, 99) stroke(ring, color, 1.7) bar(root, 1.7 * s, 5 * s, 4 * s, 4 * s, -45, color) elseif kind == "code" then text({ Size = UDim2.fromScale(1, 1), Text = "", Font = Enum.Font.Code, TextSize = math.max(8, math.floor(11 * s)), TextColor3 = color, TextXAlignment = Enum.TextXAlignment.Center, TextYAlignment = Enum.TextYAlignment.Center, TextWrapped = false }, root) elseif kind == "person" then local head = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.5, -3 * s), Size = UDim2.fromOffset(6 * s, 6 * s), BackgroundColor3 = color, BorderSizePixel = 0 }, root) round(head, 99) local body = new("Frame", { AnchorPoint = Vector2.new(0.5, 1), Position = UDim2.new(0.5, 0, 0.5, 5 * s), Size = UDim2.fromOffset(12 * s, 8 * s), BackgroundColor3 = color, BorderSizePixel = 0 }, root) round(body, 99) end return root end function recolor(root, color) for _, d in ipairs(root:GetDescendants()) do if d:IsA("Frame") then d.BackgroundColor3 = color end end end local function brandLogo(parent, model, size) if model.mark then local root = new("Frame", { Name = "ModelLogo", BackgroundColor3 = model.accent, Size = UDim2.fromOffset(size, size), BorderSizePixel = 0, }, parent) round(root, math.max(4, math.floor(size * 0.28))) local letters = tostring(model.mark):upper() text({ Size = UDim2.fromScale(1, 1), Text = letters, Font = Enum.Font.GothamBold, TextSize = math.max(8, math.floor(size * 0.42)), TextColor3 = Color3.new(1, 1, 1), TextXAlignment = Enum.TextXAlignment.Center, TextYAlignment = Enum.TextYAlignment.Center, TextWrapped = false, }, root) return root end return new("ImageLabel", { Name = "ModelLogo", BackgroundTransparency = 1, Size = UDim2.fromOffset(size, size), Image = model.icon or "", ScaleType = Enum.ScaleType.Fit, ImageColor3 = Color3.new(1, 1, 1), }, parent) end local small = camera.ViewportSize.X < 700 local SIDEBAR_W = small and math.max(150, math.floor(camera.ViewportSize.X * 0.82)) or 236 local conversations, current = {}, nil local projects, currentProject = {}, nil local modelIndex, thinkOn = 1, false local busy, cancelFlag, sidebarOpen = false, false, not small local newFolder local openSettings local settings = { console = true, screen = true, web = true, theme = "Claude", uiScale = 1, personality = "", apiKey = "", opencodeApiKey = "", openrouterKey = "", provider = "opencode", autoLogo = true, permission = "ask", motion = "Full", transcriptWidth = 740, } local API = {} local AGENTROUTER_KEY = "" local OPENCODE_KEY = "" API.AGENTROUTER_URL = "https://agentrouter.org/v1/chat/completions" API.AGENTROUTER_RESPONSES_URL = "https://agentrouter.org/v1/responses" API.AGENTROUTER_MESSAGES_URL = "https://agentrouter.org/v1/messages" API.OPENCODE_URL = "https://opencode.ai/zen/v1/chat/completions" API.OPENCODE_INFERENCE_URL = "https://opencode.ai/inference/openai/v1/chat/completions" API.OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" API.OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models" API.OPENCODE_MODELS_URL = "https://opencode.ai/zen/v1/models" API.AGENTROUTER_MODELS_URL = "https://agentrouter.org/v1/models" API.OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key" API.OPENROUTER_SPOOF = { ["HTTP-Referer"] = "https://roblox.local", ["X-Title"] = "Roblox AI Assistant" } function API.withIdentity(fn) if type(fn) ~= "function" then return nil end local getter = getthreadidentity or (syn and syn.getthreadidentity) local setter = setthreadidentity or (syn and syn.setthreadidentity) if type(setter) ~= "function" then return fn() end local current = nil if type(getter) == "function" then local ok, value = pcall(getter) if ok then current = value end end pcall(setter, 8) local results = table.pack(pcall(fn)) if current ~= nil then pcall(setter, current) end if not results[1] then error(results[2], 0) end return table.unpack(results, 2, results.n) end local opencodeBlockedUntil = 0 local webTainted = false local testApiConnection local SETTINGS_FILE = "AgentRouterRobloxAI.json" -- Game Detection -- local gameName = "Unknown Game" local placeId = game.PlaceId pcall(function() local info = MarketplaceService:GetProductInfo(placeId) if info and info.Name then gameName = info.Name end end) local ACTIVE_PALETTE = {} for k, v in pairs(C) do ACTIVE_PALETTE[k] = v end local THEMES = { Claude = { bg = Color3.fromRGB(24, 23, 21), sidebar = Color3.fromRGB(19, 18, 17), panel = Color3.fromRGB(33, 32, 30), hover = Color3.fromRGB(44, 42, 39), userBubble = Color3.fromRGB(36, 34, 31), code = Color3.fromRGB(16, 15, 14), stroke = Color3.fromRGB(56, 53, 49), text = Color3.fromRGB(247, 245, 240), subtext = Color3.fromRGB(156, 151, 143), error = Color3.fromRGB(235, 110, 110) }, ChatGPT = { bg = Color3.fromRGB(32, 33, 35), sidebar = Color3.fromRGB(23, 24, 25), panel = Color3.fromRGB(43, 44, 46), hover = Color3.fromRGB(57, 58, 61), userBubble = Color3.fromRGB(36, 36, 38), code = Color3.fromRGB(18, 18, 19), stroke = Color3.fromRGB(58, 59, 62), text = Color3.fromRGB(238, 238, 238), subtext = Color3.fromRGB(150, 151, 153), error = Color3.fromRGB(255, 100, 100) }, DeepSeek = { bg = Color3.fromRGB(20, 25, 33), sidebar = Color3.fromRGB(14, 18, 24), panel = Color3.fromRGB(29, 34, 44), hover = Color3.fromRGB(41, 49, 61), userBubble = Color3.fromRGB(26, 32, 42), code = Color3.fromRGB(11, 14, 19), stroke = Color3.fromRGB(47, 57, 72), text = Color3.fromRGB(223, 231, 241), subtext = Color3.fromRGB(131, 143, 157), error = Color3.fromRGB(255, 106, 106) }, GLM = { bg = Color3.fromRGB(18, 24, 27), sidebar = Color3.fromRGB(13, 17, 20), panel = Color3.fromRGB(27, 34, 38), hover = Color3.fromRGB(38, 47, 52), userBubble = Color3.fromRGB(23, 30, 34), code = Color3.fromRGB(9, 12, 14), stroke = Color3.fromRGB(44, 55, 61), text = Color3.fromRGB(226, 235, 238), subtext = Color3.fromRGB(130, 145, 150), error = Color3.fromRGB(245, 112, 124) }, Kimi = { bg = Color3.fromRGB(23, 21, 28), sidebar = Color3.fromRGB(17, 15, 22), panel = Color3.fromRGB(32, 29, 40), hover = Color3.fromRGB(44, 40, 55), userBubble = Color3.fromRGB(29, 26, 37), code = Color3.fromRGB(12, 11, 16), stroke = Color3.fromRGB(53, 49, 66), text = Color3.fromRGB(234, 231, 241), subtext = Color3.fromRGB(144, 139, 158), error = Color3.fromRGB(245, 112, 124) }, } local PERMISSION_MODES = { { id = "readonly", title = "Read only", desc = "Inspection only. Nothing is changed, executed or written." }, { id = "ask", title = "Ask first", desc = "Reads run freely. Anything that changes the game waits for you." }, { id = "auto", title = "Auto (ask for dangerous)", desc = "Reads and writes run freely. Code execution and deletion still ask." }, { id = "allow", title = "Allow everything", desc = "No prompts at all. Only sensible when you trust the provider.", accent = true }, } function permissionInfo(id) for _, p in ipairs(PERMISSION_MODES) do if p.id == id then return p end end return PERMISSION_MODES[2] end function loadSavedSettings() if type(readfile) ~= "function" or type(isfile) ~= "function" then return end local ok, data = pcall(function() if not isfile(SETTINGS_FILE) then return nil end return HttpService:JSONDecode(readfile(SETTINGS_FILE)) end) if ok and type(data) == "table" then for _, key in ipairs({ "console", "screen", "web", "theme", "uiScale", "personality", "apiKey", "opencodeApiKey", "openrouterKey", "provider", "autoLogo", "permission", "motion", "transcriptWidth", "autoCompact", "customModel", "modelId" }) do if data[key] ~= nil then settings[key] = data[key] end end end end function saveSettings() if type(writefile) ~= "function" then return false end local ok = pcall(function() writefile(SETTINGS_FILE, HttpService:JSONEncode(settings)) end) return ok end loadSavedSettings() AGENTROUTER_KEY = settings.apiKey or "" OPENCODE_KEY = settings.opencodeApiKey or "" MOTION = settings.motion == "Off" and 0 or (settings.motion == "Subtle" and 0.6 or 1) local attachments, MAX_ATTACH = {}, 3 local gui = (function() local props = { Name = "AIChatUI", ResetOnSpawn = false, IgnoreGuiInset = true, ZIndexBehavior = Enum.ZIndexBehavior.Sibling, DisplayOrder = 100 } local ok, built = pcall(function() local container = gethui and gethui() if container then return new("ScreenGui", props, container) end return nil end) if ok and built then return built end return new("ScreenGui", props, playerGui) end)() local window = new("Frame", { Name = "Window", AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromScale(0.88, 0.9), BackgroundColor3 = C.bg, }, gui) round(window, 16) stroke(window) local windowConstraint = new("UISizeConstraint", { MinSize = Vector2.new(180, 160), MaxSize = Vector2.new(980, 640) }, window) local winScale = new("UIScale", { Scale = 1 }, window) local main = new("Frame", { Name = "Main", BackgroundTransparency = 1, Position = UDim2.fromOffset(SIDEBAR_W, 0), Size = UDim2.new(1, -SIDEBAR_W, 1, 0), }, window) local header = new("Frame", { Name = "Header", BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 52) }, main) local loadingBar = new("Frame", { Name = "LoadingBar", Position = UDim2.new(0, 0, 0, 50), Size = UDim2.new(0.18, 0, 0, 2), BackgroundColor3 = MODELS[1].accent, BorderSizePixel = 0, ZIndex = 20 }, main) loadingBar.Visible = false local loadingActive = false function setLoading(active) loadingActive = active loadingBar.Visible = active if active then task.spawn(function() while loadingActive and loadingBar.Parent do loadingBar.Position = UDim2.new(-0.2, 0, 0, 50) loadingBar.BackgroundColor3 = MODELS[modelIndex].accent tween(loadingBar, 0.7, { Position = UDim2.new(1, 0, 0, 50) }) task.wait(0.75) end loadingBar.Visible = false end) end end local menuBtn = button({ Position = UDim2.fromOffset(10, 10), Size = UDim2.fromOffset(32, 32), BackgroundColor3 = C.hover }, header) round(menuBtn, 8) hover(menuBtn, C.hover, 0) icon("menu", menuBtn, C.subtext, 20) local titleLabel = text({ Position = UDim2.fromOffset(54, 4), Size = UDim2.new(1, -150, 0, 24), Text = "New chat", Font = Enum.Font.GothamMedium, TextSize = 15, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, }, header) local subtitleLabel = text({ Position = UDim2.fromOffset(54, 27), Size = UDim2.new(1, -150, 0, 16), Text = "", TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, }, header) local function updateHeaderSubtitle() local m = MODELS[modelIndex] local proj = currentProject and currentProject.name or "Chat" subtitleLabel.Text = proj .. " · " .. m.brandName .. " " .. m.short end local minBtn = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -46, 0, 10), Size = UDim2.fromOffset(32, 32), BackgroundColor3 = C.hover }, header) round(minBtn, 8); hover(minBtn, C.hover, 0); icon("minus", minBtn, C.subtext, 20) local closeBtn = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -10, 0, 10), Size = UDim2.fromOffset(32, 32), BackgroundColor3 = C.hover }, header) round(closeBtn, 8); hover(closeBtn, C.hover, 0); icon("cross", closeBtn, C.subtext, 18) local messageArea = new("ScrollingFrame", { Name = "Messages", Position = UDim2.fromOffset(0, 52), Size = UDim2.new(1, 0, 1, -170), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 4, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, ScrollingDirection = Enum.ScrollingDirection.Y, }, main) local column = new("Frame", { Name = "Column", AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.fromScale(0.5, 0), Size = UDim2.new(1, -32, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, }, messageArea) new("UISizeConstraint", { MaxSize = Vector2.new(740, math.huge) }, column) new("UIListLayout", { Padding = UDim.new(0, 20), SortOrder = Enum.SortOrder.LayoutOrder }, column) pad(column, 16, 0, 24, 0) local composer = new("Frame", { Name = "Composer", AnchorPoint = Vector2.new(0.5, 1), Position = UDim2.new(0.5, 0, 1, -14), Size = UDim2.new(1, -32, 0, 92), BackgroundColor3 = C.panel, }, main) round(composer, 18) stroke(composer) new("UISizeConstraint", { MaxSize = Vector2.new(740, math.huge) }, composer) local input = new("TextBox", { Name = "Input", Position = UDim2.fromOffset(16, 14), Size = UDim2.new(1, -32, 0, 24), BackgroundTransparency = 1, Text = "", PlaceholderText = "Message...", PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = 15, Font = Enum.Font.Gotham, TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, ClearTextOnFocus = false, MultiLine = true, TextWrapped = true, }, composer) local attachRow = new("Frame", { Name = "Attachments", Position = UDim2.fromOffset(12, 10), Size = UDim2.new(1, -24, 0, 58), BackgroundTransparency = 1, Visible = false, }, composer) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder }, attachRow) local row = new("Frame", { AnchorPoint = Vector2.new(0, 1), Position = UDim2.new(0, 10, 1, -8), Size = UDim2.new(1, -20, 0, 34), BackgroundTransparency = 1, }, composer) local plusBtn = button({ AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, 0, 0.5, 0), Size = UDim2.fromOffset(32, 32), BackgroundColor3 = C.panel, }, row) round(plusBtn, 16) stroke(plusBtn, C.stroke) hover(plusBtn, C.hover, 0) icon("plus", plusBtn, C.subtext, 18) local thinkPill = button({ AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, 40, 0.5, 0), Size = UDim2.fromOffset(0, 32), AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.panel, }, row) round(thinkPill, 16) local thinkStroke = stroke(thinkPill, C.stroke) pad(thinkPill, 0, 14, 0, 28) local thinkDot = new("Frame", { AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, -17, 0.5, 0), Size = UDim2.fromOffset(8, 8), BackgroundColor3 = C.subtext, }, thinkPill) round(thinkDot, 4) local thinkLabel = text({ Size = UDim2.new(0, 0, 1, 0), AutomaticSize = Enum.AutomaticSize.X, Text = "Think", TextSize = 13, Font = Enum.Font.GothamMedium, TextColor3 = C.subtext, TextWrapped = false, }, thinkPill) local sendBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(34, 34), BackgroundColor3 = MODELS[1].accent, }, row) round(sendBtn, 17) local arrowIcon = icon("arrowUp", sendBtn, Color3.new(1, 1, 1), 20) local stopIcon = icon("stop", sendBtn, Color3.new(1, 1, 1), 20) stopIcon.Visible = false local modelBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -72, 0.5, 0), Size = UDim2.fromOffset(140, 30), BackgroundColor3 = C.panel, }, row) round(modelBtn, 15) stroke(modelBtn, C.stroke) hover(modelBtn, C.hover, 0) local modelDot = new("Frame", { AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, 11, 0.5, 0), Size = UDim2.fromOffset(7, 7), BackgroundColor3 = Color3.fromRGB(106, 200, 145), BorderSizePixel = 0, }, modelBtn) round(modelDot, 4) local modelName = text({ Position = UDim2.fromOffset(23, 0), Size = UDim2.new(0, 58, 1, 0), Text = "DS Free", TextSize = 12, Font = Enum.Font.GothamMedium, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, }, modelBtn) local contextPct = text({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -26, 0.5, 0), Size = UDim2.fromOffset(30, 20), Text = "0%", TextSize = 11, TextColor3 = Color3.fromRGB(106, 200, 145), TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false, }, modelBtn) local chev = new("Frame", { BackgroundTransparency = 1, AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -8, 0.5, 0), Size = UDim2.fromOffset(14, 14) }, modelBtn) icon("chevron", chev, C.subtext, 14) local moreBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -40, 0.5, 0), Size = UDim2.fromOffset(26, 30), BackgroundColor3 = C.panel }, row) round(moreBtn, 8) stroke(moreBtn, C.stroke) hover(moreBtn, C.hover, 0) do local dots = new("Frame", { BackgroundTransparency = 1, AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(16, 16) }, moreBtn) for k = 1, 3 do local d = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, (k - 2) * 5, 0.5, 0), Size = UDim2.fromOffset(2.5, 2.5), BackgroundColor3 = C.subtext, BorderSizePixel = 0 }, dots) round(d, 2) end end local function updateModelChip() local m = MODELS[modelIndex] modelName.Text = m.short local chars = 0 if current then for _, msg in ipairs(current.messages) do if msg.text then chars += #msg.text end for _, t in ipairs(msg.tools or {}) do if t.label then chars += #t.label end end if msg.images then chars += #msg.images * 4000 end end end local pct = math.clamp(math.floor((chars / 4) / (m.context or 1000000) * 100 + 0.5), 0, 99) contextPct.Text = pct .. "%" local col = pct >= 85 and C.error or (pct >= 60 and Color3.fromRGB(230, 180, 80) or Color3.fromRGB(106, 200, 145)) contextPct.TextColor3 = col modelDot.BackgroundColor3 = col end local sidebar = new("Frame", { Name = "Sidebar", Size = UDim2.new(0, SIDEBAR_W, 1, 0), BackgroundColor3 = C.sidebar, ClipsDescendants = true, ZIndex = small and 20 or 1, }, window) round(sidebar, 16) new("Frame", { Size = UDim2.new(0, 16, 1, 0), Position = UDim2.new(1, -16, 0, 0), BackgroundColor3 = C.sidebar, BorderSizePixel = 0 }, sidebar) new("Frame", { Size = UDim2.new(0, 1, 1, 0), Position = UDim2.new(1, -1, 0, 0), BackgroundColor3 = C.stroke, BorderSizePixel = 0 }, sidebar) local sideInner = new("Frame", { BackgroundTransparency = 1, Size = UDim2.new(0, SIDEBAR_W, 1, 0) }, sidebar) local brandSpark = brandLogo(sideInner, MODELS[1], 22) brandSpark.Position = UDim2.fromOffset(18, 17) text({ Position = UDim2.fromOffset(50, 12), Size = UDim2.fromOffset(100, 32), Text = "Chat", Font = SERIF, TextSize = 20, TextWrapped = false }, sideInner) local sideCloseBtn = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -14, 0, 12), Size = UDim2.fromOffset(28, 28), BackgroundColor3 = C.hover, }, sideInner) round(sideCloseBtn, 8) hover(sideCloseBtn, C.hover, 0) icon("cross", sideCloseBtn, C.subtext, 14) local newBtn = button({ Position = UDim2.fromOffset(12, 50), Size = UDim2.new(1, -24, 0, 36), BackgroundColor3 = C.panel, }, sideInner) round(newBtn, 10) stroke(newBtn, C.stroke) hover(newBtn, C.hover, 0) local plusHolder = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(10, 9), Size = UDim2.fromOffset(20, 20) }, newBtn) local plusIcon = icon("plus", plusHolder, MODELS[1].accent, 16) text({ Position = UDim2.fromOffset(38, 0), Size = UDim2.new(1, -44, 1, 0), Text = "New chat", Font = Enum.Font.GothamMedium, TextSize = 14, TextWrapped = false }, newBtn) local searchBox = new("TextBox", { Position = UDim2.fromOffset(12, 104), Size = UDim2.new(1, -24, 0, 32), BackgroundColor3 = C.bg, Text = "", PlaceholderText = "Search chats", PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = 13, Font = Enum.Font.Gotham, ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left, }, sideInner) round(searchBox, 8) pad(searchBox, 0, 10, 0, 12) local convoList = new("ScrollingFrame", { Position = UDim2.fromOffset(8, 210), Size = UDim2.new(1, -16, 1, -284), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, }, sideInner) new("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }, convoList) local sideProviderLabel local userCard = new("Frame", { Position = UDim2.new(0, 10, 1, -58), Size = UDim2.new(1, -20, 0, 48), BackgroundColor3 = C.panel, }, sideInner) round(userCard, 12) local avatar = new("ImageLabel", { Position = UDim2.fromOffset(8, 8), Size = UDim2.fromOffset(32, 32), BackgroundColor3 = C.hover, Image = "" }, userCard) round(avatar, 16) task.spawn(function() local ok, img = pcall(function() return Players:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size100x100) end) if ok then avatar.Image = img end end) text({ Position = UDim2.fromOffset(48, 6), Size = UDim2.new(1, -56, 0, 20), Text = player.DisplayName, Font = Enum.Font.GothamMedium, TextSize = 13, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, userCard) sideProviderLabel = text({ Position = UDim2.fromOffset(48, 24), Size = UDim2.new(1, -56, 0, 16), Text = providerLabel(MODELS[1]), TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, userCard) local settingsBtn, layoutSidebar, workspaceNav local workspacePanel, toggleWorkspace, workspaceRefresh do local nav = new("Frame", { Name = "Nav", Position = UDim2.fromOffset(8, 98), Size = UDim2.new(1, -16, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1 }, sideInner) new("UIListLayout", { Padding = UDim.new(0, 1), SortOrder = Enum.SortOrder.LayoutOrder }, nav) local order, subs = 0, {} local function navRow(label, kind, indent) indent = indent or 0 order = order + 1 local row = button({ Size = UDim2.new(1, 0, 0, 28), LayoutOrder = order }, nav) round(row, 7) hover(row, C.hover, 0.45) local ic = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(10 + indent, 6), Size = UDim2.fromOffset(16, 16) }, row) icon(kind, ic, C.subtext, 16) text({ Position = UDim2.fromOffset(34 + indent, 0), Size = UDim2.new(1, -44 - indent, 1, 0), Text = label, TextSize = 13, TextColor3 = C.subtext, TextWrapped = false }, row) return row end navRow("Customize", "settings").MouseButton1Click:Connect(function() openSettings("Appearance") end) workspaceNav = navRow("Workspace", "folder") workspaceNav.MouseButton1Click:Connect(function() if toggleWorkspace then toggleWorkspace() end end) local moreRow = navRow("More", "code") local chevIcon = icon("chevron", moreRow, C.subtext, 12) chevIcon.AnchorPoint = Vector2.new(1, 0.5) chevIcon.Position = UDim2.new(1, -12, 0.5, 0) chevIcon.Rotation = -90 local function subRow(label, kind, page) local row = navRow(label, kind, 14) row.Visible = false row.MouseButton1Click:Connect(function() openSettings(page) end) table.insert(subs, row) end subRow("Providers", "globe", "Providers") subRow("Tools", "terminal", "Permissions") settingsBtn = navRow("Settings", "settings") settingsBtn.MouseButton1Click:Connect(function() openSettings("General") end) local moreOpen = false moreRow.MouseButton1Click:Connect(function() moreOpen = not moreOpen for _, r in ipairs(subs) do r.Visible = moreOpen end tween(chevIcon, 0.16, { Rotation = moreOpen and 0 or -90 }) task.defer(function() layoutSidebar() end) end) layoutSidebar = function() local visible = 0 for _, ch in ipairs(nav:GetChildren()) do if ch:IsA("GuiObject") and ch.Visible then visible = visible + 1 end end local top = 98 + visible * 28 + math.max(0, visible - 1) + 10 searchBox.Position = UDim2.fromOffset(12, top) convoList.Position = UDim2.fromOffset(8, top + 42) local total = sideInner.AbsoluteSize.Y if total <= 0 then total = window.AbsoluteSize.Y end if total <= 0 then total = 560 end local height = total - (top + 42) - 74 if height < 60 then height = 60 end convoList.Size = UDim2.new(1, -16, 0, height) end sideInner:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() layoutSidebar() end) nav:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() layoutSidebar() end) task.defer(function() layoutSidebar() end) end local toggleBtn = button({ AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, 12, 0.5, 0), Size = UDim2.fromOffset(46, 46), BackgroundColor3 = C.sidebar, }, gui) round(toggleBtn, 23) stroke(toggleBtn) local toggleSpark = brandLogo(toggleBtn, MODELS[1], 22) toggleSpark.Position = UDim2.fromOffset(12, 12) do local WS = { expanded = {}, selected = nil, order = 0, view = "tree", source = nil, cache = { at = 0, items = nil } } local SCRIPT_CLASSES = { Script = true, LocalScript = true, ModuleScript = true } local REMOTE_CLASSES = { RemoteEvent = true, RemoteFunction = true, BindableEvent = true, BindableFunction = true, UnreliableRemoteEvent = true } local ROWS_CAP = 300 local root = new("Frame", { Name = "Workspace", Position = UDim2.fromOffset(0, 52), Size = UDim2.new(1, 0, 1, -52), BackgroundColor3 = C.bg, Visible = false, ZIndex = 8 }, main) workspacePanel = root local bar = new("Frame", { Size = UDim2.new(1, 0, 0, 40), BackgroundColor3 = C.bg }, root) new("Frame", { Position = UDim2.new(0, 0, 1, -1), Size = UDim2.new(1, 0, 0, 1), BackgroundColor3 = C.stroke, BorderSizePixel = 0 }, bar) local tabs = new("Frame", { Position = UDim2.fromOffset(10, 9), Size = UDim2.fromOffset(170, 22), BackgroundTransparency = 1 }, bar) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center }, tabs) local chipButtons = {} for i, name in ipairs({ "Tree", "Scripts", "Remotes" }) do local chip = button({ Size = UDim2.fromOffset(52, 22), BackgroundColor3 = C.hover, Text = name, TextSize = 10, LayoutOrder = i }, tabs) round(chip, 6) hover(chip, C.hover, 0) chipButtons[name] = chip end local filter = new("TextBox", { Position = UDim2.fromOffset(188, 9), Size = UDim2.new(1, -374, 0, 22), BackgroundColor3 = C.panel, Text = "", PlaceholderText = "Filter by name or class", PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = 11, TextXAlignment = Enum.TextXAlignment.Left, ClearTextOnFocus = false }, bar) round(filter, 6) pad(filter, 0, 8, 0, 8) local collapseBtn = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -114, 0, 9), Size = UDim2.fromOffset(62, 22), BackgroundColor3 = C.panel }, bar) round(collapseBtn, 6) hover(collapseBtn, C.hover, 0) text({ Size = UDim2.fromScale(1, 1), Text = "Collapse", TextSize = 10, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Center, TextWrapped = false }, collapseBtn) local refreshBtn = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -46, 0, 9), Size = UDim2.fromOffset(62, 22), BackgroundColor3 = C.panel }, bar) round(refreshBtn, 6) hover(refreshBtn, C.hover, 0) text({ Size = UDim2.fromScale(1, 1), Text = "Refresh", TextSize = 10, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Center, TextWrapped = false }, refreshBtn) local closeWs = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -10, 0, 9), Size = UDim2.fromOffset(26, 22), BackgroundColor3 = C.hover }, bar) round(closeWs, 6) hover(closeWs, C.hover, 0) icon("cross", closeWs, C.subtext, 12) local list = new("ScrollingFrame", { Position = UDim2.fromOffset(0, 40), Size = UDim2.new(0.56, 0, 1, -40), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y }, root) new("UIListLayout", { Padding = UDim.new(0, 1), SortOrder = Enum.SortOrder.LayoutOrder }, list) local detail = new("Frame", { Position = UDim2.new(0.56, 0, 0, 40), Size = UDim2.new(0.44, 0, 1, -40), BackgroundColor3 = C.sidebar }, root) new("Frame", { Size = UDim2.new(0, 1, 1, 0), BackgroundColor3 = C.stroke, BorderSizePixel = 0 }, detail) local title = text({ Position = UDim2.fromOffset(12, 10), Size = UDim2.new(1, -24, 0, 18), Text = "No instance selected", Font = Enum.Font.GothamBold, TextSize = 12, TextColor3 = C.text, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, detail) local subtitle = text({ Position = UDim2.fromOffset(12, 28), Size = UDim2.new(1, -24, 0, 16), Text = "Pick something to inspect it.", TextSize = 10, Font = Enum.Font.Code, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, detail) local propList = new("ScrollingFrame", { Position = UDim2.fromOffset(8, 50), Size = UDim2.new(1, -16, 1, -58), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y }, detail) new("UIListLayout", { Padding = UDim.new(0, 2), SortOrder = Enum.SortOrder.LayoutOrder }, propList) local sourceScroll = new("ScrollingFrame", { Position = UDim2.fromOffset(8, 50), Size = UDim2.new(1, -16, 1, -58), BackgroundColor3 = C.code, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, Visible = false }, detail) round(sourceScroll, 6) local sourceLabel = text({ Position = UDim2.fromOffset(10, 8), Size = UDim2.new(1, -20, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, Text = "", Font = Enum.Font.Code, TextSize = 11, TextColor3 = C.text, TextWrapped = false }, sourceScroll) local copySource = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -10, 0, 8), Size = UDim2.fromOffset(48, 18), BackgroundColor3 = C.hover, Text = "Copy", TextSize = 9, Visible = false, ZIndex = 3 }, detail) round(copySource, 5) hover(copySource, C.panel, 0) local function classNameOf(inst) local ok, value = pcall(function() return inst.ClassName end) if ok and type(value) == "string" and value ~= "" then return value end return "Instance" end local function pathOf(inst) local ok, full = pcall(function() return inst:GetFullName() end) if ok and type(full) == "string" and full ~= "" then return full end return tostring(inst.Name or "?") end local function formatValue(v) local t = typeof(v) if t == "Instance" then return pathOf(v) end if t == "Vector3" then return string.format("(%.2f, %.2f, %.2f)", v.X, v.Y, v.Z) end if t == "Vector2" then return string.format("(%.2f, %.2f)", v.X, v.Y) end if t == "Color3" then return string.format("%d, %d, %d", math.floor(v.R * 255 + 0.5), math.floor(v.G * 255 + 0.5), math.floor(v.B * 255 + 0.5)) end if t == "UDim2" then return string.format("%d,%d,%d,%d", v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset) end if t == "CFrame" then local p = v.Position return string.format("(%.1f, %.1f, %.1f)", p.X, p.Y, p.Z) end local ok, s = pcall(tostring, v) if not ok then return "<" .. t .. ">" end if #s > 90 then s = s:sub(1, 87) .. "..." end return s end local function parseValue(raw, current) local value = tostring(raw or "") local t = typeof(current) if t == "boolean" then return value:lower() == "true" or value == "1" end if t == "number" then return tonumber(value) end if t == "Vector3" then local x, y, z = value:match("([%-%d%.]+)%s*,%s*([%-%d%.]+)%s*,%s*([%-%d%.]+)") if x then return Vector3.new(tonumber(x), tonumber(y), tonumber(z)) end return current end if t == "Vector2" then local x, y = value:match("([%-%d%.]+)%s*,%s*([%-%d%.]+)") if x then return Vector2.new(tonumber(x), tonumber(y)) end return current end if t == "Color3" then local r, g, b = value:match("(%d+)%s*,%s*(%d+)%s*,%s*(%d+)") if r then return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b)) end return current end if t == "UDim2" then local xs, xo, ys, yo = value:match("([%-%d%.]+)%s*,%s*([%-%d%.]+)%s*,%s*([%-%d%.]+)%s*,%s*([%-%d%.]+)") if xs then return UDim2.new(tonumber(xs), tonumber(xo), tonumber(ys), tonumber(yo)) end return current end return value end local KNOWN_PROPS = { "Name", "Position", "Size", "CFrame", "Anchored", "CanCollide", "Transparency", "Color", "BackgroundColor3", "BackgroundTransparency", "Text", "TextColor3", "TextSize", "Visible", "Enabled", "Value", "Health", "MaxHealth", "WalkSpeed", "JumpPower", "Material", "CanTouch", "Shape", "Orientation", "Massless", "Locked", "Disabled", "Parent" } local function propsFor(inst) local list, seen = {}, {} local ok, props = pcall(function() return inst:GetProperties() end) if ok and type(props) == "table" then for _, name in ipairs(props) do if not seen[name] then seen[name] = true local okGet = pcall(function() return inst[name] end) if okGet then list[#list + 1] = name end end end end for _, name in ipairs(KNOWN_PROPS) do if not seen[name] then local okGet = pcall(function() return inst[name] end) if okGet then seen[name] = true list[#list + 1] = name end end end table.sort(list) return list end local function allInstances() local now = os.clock() if WS.cache.items and now - WS.cache.at < 6 then return WS.cache.items end local items, frontier, guard = {}, { game }, 0 while #frontier > 0 and #items < 5000 and guard < 6000 do guard = guard + 1 local node = table.remove(frontier, 1) local ok, children = pcall(function() return node:GetChildren() end) if ok and type(children) == "table" then for _, child in ipairs(children) do items[#items + 1] = child local okKids, kids = pcall(function() return child:GetChildren() end) if okKids and type(kids) == "table" and #kids > 0 then frontier[#frontier + 1] = child end end end end WS.cache.items, WS.cache.at = items, now return items end local function readSource(inst) local ok, value = pcall(function() return API.withIdentity(function() return inst.Source end) end) if not ok then return nil, tostring(value):sub(1, 180) end if type(value) ~= "string" or value == "" then return nil, "No readable Source on this instance." end return value end local function clearDetail() for _, ch in ipairs(propList:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end end local function propRow(order, name, inst, current, editable) local rowBox = new("Frame", { Size = UDim2.new(1, 0, 0, 24), BackgroundColor3 = C.panel, LayoutOrder = order }, propList) round(rowBox, 5) text({ Position = UDim2.fromOffset(8, 4), Size = UDim2.new(0.42, -8, 0, 16), Text = name, TextSize = 10, Font = Enum.Font.Code, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, rowBox) local valueLabel = text({ Position = UDim2.new(0.42, 0, 0, 0), Size = UDim2.new(0.58, -46, 1, 0), Text = formatValue(current), TextSize = 10, Font = Enum.Font.Code, TextColor3 = C.text, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, rowBox) if not editable then return end local editor = new("TextBox", { Position = UDim2.new(0.42, -2, 0.5, 0), Size = UDim2.new(0.58, -44, 0, 18), BackgroundColor3 = C.bg, BackgroundTransparency = 1, Text = "", TextSize = 10, Font = Enum.Font.Code, TextColor3 = C.text, ClearTextOnFocus = false, Visible = false }, rowBox) pad(editor, 0, 5, 0, 3) local editBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -5, 0.5, 0), Size = UDim2.fromOffset(36, 18), BackgroundColor3 = C.hover, Text = "Edit", TextSize = 9 }, rowBox) round(editBtn, 4) hover(editBtn, C.bg, 0) local editing = false local function commit() if not editing then return end editing = false editor.Visible = false valueLabel.Visible = true local typed = editor.Text local okSet = pcall(function() inst[name] = parseValue(typed, inst[name]) end) if okSet then valueLabel.Text = formatValue(inst[name]) else valueLabel.Text = "cannot set" end end editor.FocusLost:Connect(commit) editBtn.MouseButton1Click:Connect(function() editing = true editor.Text = formatValue(inst[name]) valueLabel.Visible = false editor.Visible = true editor:CaptureFocus() end) end local function showProps(inst) WS.selected = inst sourceScroll.Visible = false copySource.Visible = false propList.Visible = true title.Text = inst.Name .. " · " .. classNameOf(inst) subtitle.Text = pathOf(inst) clearDetail() local order = 0 order = order + 1 propRow(order, "ClassName", inst, classNameOf(inst), false) order = order + 1 propRow(order, "Name", inst, inst.Name, true) for _, name in ipairs(propsFor(inst)) do if name ~= "ClassName" and name ~= "Name" and name ~= "Parent" then local okGet, current = pcall(function() return inst[name] end) if okGet then order = order + 1 propRow(order, name, inst, current, true) end end end local kids = {} pcall(function() kids = inst:GetChildren() end) order = order + 1 local kidRow = new("Frame", { Size = UDim2.new(1, 0, 0, 22), BackgroundTransparency = 1, LayoutOrder = order }, propList) text({ Size = UDim2.new(1, -8, 1, 0), Text = tostring(#kids) .. " children", TextSize = 10, TextColor3 = C.subtext, TextWrapped = false }, kidRow) for i, child in ipairs(kids) do if i > 12 then order = order + 1 local more = new("Frame", { Size = UDim2.new(1, 0, 0, 18), BackgroundTransparency = 1, LayoutOrder = order }, propList) text({ Size = UDim2.new(1, -8, 1, 0), Text = " + " .. tostring(#kids - 12) .. " more", TextSize = 9, TextColor3 = C.subtext, TextWrapped = false }, more) break end order = order + 1 local childBtn = button({ Size = UDim2.new(1, 0, 0, 20), BackgroundTransparency = 1, LayoutOrder = order }, propList) round(childBtn, 4) hover(childBtn, C.hover, 0.5) pad(childBtn, 0, 6, 0, 8) text({ Position = UDim2.fromOffset(4, 3), Size = UDim2.new(1, -8, 0, 14), Text = child.Name, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, childBtn) childBtn.MouseButton1Click:Connect(function() if SCRIPT_CLASSES[classNameOf(child)] then showSource(child) else showProps(child) end end) end end local function showSource(inst) WS.selected = inst propList.Visible = false sourceScroll.Visible = true copySource.Visible = true sourceScroll.CanvasPosition = Vector2.new(0, 0) title.Text = inst.Name .. " · " .. classNameOf(inst) subtitle.Text = pathOf(inst) clearDetail() local source, err = readSource(inst) if not source then WS.source = nil copySource.Visible = false sourceLabel.TextColor3 = C.subtext sourceLabel.Text = "-- " .. tostring(err or "Source is not readable here.") return end WS.source = source sourceLabel.TextColor3 = C.text local shown = source if #shown > 20000 then shown = shown:sub(1, 20000) .. "\n-- [truncated here, Copy still copies the whole script]" end sourceLabel.Text = shown end local function select(inst) if SCRIPT_CLASSES[classNameOf(inst)] then showSource(inst) else showProps(inst) end end local function treeRow(depth, inst) local children = {} pcall(function() children = inst:GetChildren() end) WS.order = WS.order + 1 local item = button({ Size = UDim2.new(1, 0, 0, 20), BackgroundTransparency = 1, LayoutOrder = WS.order }, list) round(item, 4) hover(item, C.hover, 0.4) pad(item, 0, 6, 0, 4 + depth * 10) if #children > 0 then local chev = button({ Position = UDim2.fromOffset(2, 3), Size = UDim2.fromOffset(14, 14), BackgroundTransparency = 1 }, item) local arrow = icon("chevron", chev, WS.selected == inst and C.text or C.subtext, 8) arrow.Rotation = WS.expanded[inst] and 90 or 0 chev.MouseButton1Click:Connect(function() WS.expanded[inst] = not WS.expanded[inst] if WS.render then WS.render() end end) end local dot = new("Frame", { Position = UDim2.fromOffset(19, 7), Size = UDim2.fromOffset(5, 5), BackgroundColor3 = WS.selected == inst and MODELS[modelIndex].accent or C.stroke, BorderSizePixel = 0 }, item) round(dot, 3) text({ Position = UDim2.fromOffset(30, 0), Size = UDim2.new(1, -56, 1, 0), Text = inst.Name, TextSize = 10, TextColor3 = WS.selected == inst and C.text or C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, item) if #children > 0 then text({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -8, 0.5, 0), Size = UDim2.fromOffset(28, 14), Text = tostring(#children), TextSize = 9, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false }, item) end item.MouseButton1Click:Connect(function() WS.selected = inst if #children > 0 and not WS.expanded[inst] then WS.expanded[inst] = true end if WS.render then WS.render() end select(inst) end) end local function flatRow(inst, hint) WS.order = WS.order + 1 local item = button({ Size = UDim2.new(1, 0, 0, 26), BackgroundTransparency = 1, LayoutOrder = WS.order }, list) round(item, 4) hover(item, C.hover, 0.4) pad(item, 0, 8, 0, 12) local dot = new("Frame", { Position = UDim2.fromOffset(2, 10), Size = UDim2.fromOffset(6, 6), BackgroundColor3 = WS.selected == inst and MODELS[modelIndex].accent or C.stroke, BorderSizePixel = 0 }, item) round(dot, 3) text({ Position = UDim2.fromOffset(14, 3), Size = UDim2.new(1, -84, 0, 14), Text = inst.Name, TextSize = 10, TextColor3 = WS.selected == inst and C.text or C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, item) text({ Position = UDim2.fromOffset(14, 14), Size = UDim2.new(1, -20, 0, 12), Text = pathOf(inst), TextSize = 8, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, item) if hint then text({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -8, 0.5, 0), Size = UDim2.fromOffset(70, 14), Text = hint, TextSize = 8, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, item) end item.MouseButton1Click:Connect(function() WS.selected = inst if WS.render then WS.render() end select(inst) end) end local function noteLabel(value) text({ Size = UDim2.new(1, 0, 0, 26), Text = " " .. value, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false }, list) end local function matches(inst, query) if query == "" then return true end if tostring(inst.Name):lower():find(query, 1, true) then return true end return classNameOf(inst):lower():find(query, 1, true) ~= nil end local function renderTree() local query = filter.Text:lower():gsub("^%s+", ""):gsub("%s+$", "") if query ~= "" then local hits = 0 for _, inst in ipairs(allInstances()) do if matches(inst, query) then hits = hits + 1 if hits > ROWS_CAP then break end treeRow(1, inst) end end if hits == 0 then noteLabel("Nothing matches that filter.") elseif hits > ROWS_CAP then noteLabel("Showing the first " .. tostring(ROWS_CAP) .. " matches.") end return end noteLabel("Root: " .. gameName) local function walk(node, depth) if depth > 12 then return end local children = {} pcall(function() children = node:GetChildren() end) for _, child in ipairs(children) do if WS.order >= ROWS_CAP then return end treeRow(depth, child) if WS.expanded[child] then walk(child, depth + 1) end end end walk(game, 1) if WS.order == 0 then noteLabel("This place has no instances") end if WS.order >= ROWS_CAP then noteLabel("Showing the first " .. tostring(ROWS_CAP) .. " nodes. Filter to narrow.") end end local function renderScripts() local query = filter.Text:lower():gsub("^%s+", ""):gsub("%s+$", "") local hits, total = {}, 0 for _, inst in ipairs(allInstances()) do if SCRIPT_CLASSES[classNameOf(inst)] then total = total + 1 if matches(inst, query) then hits[#hits + 1] = inst end end end if total == 0 then noteLabel("No scripts were found in this place.") return end table.sort(hits, function(a, b) return tostring(a.Name):lower() < tostring(b.Name):lower() end) noteLabel(tostring(total) .. (total == 1 and " script" or " scripts") .. " in this place. Select one to read its source.") for i, inst in ipairs(hits) do if i > ROWS_CAP then break end flatRow(inst, classNameOf(inst)) end if #hits == 0 then noteLabel("Nothing matches that filter.") end end local function renderRemotes() local query = filter.Text:lower():gsub("^%s+", ""):gsub("%s+$", "") local hits, total = {}, 0 for _, inst in ipairs(allInstances()) do if REMOTE_CLASSES[classNameOf(inst)] then total = total + 1 if matches(inst, query) then hits[#hits + 1] = inst end end end if total == 0 then noteLabel("No RemoteEvent, RemoteFunction or Bindable instances found.") return end table.sort(hits, function(a, b) return tostring(a.Name):lower() < tostring(b.Name):lower() end) noteLabel(tostring(total) .. (total == 1 and " remote" or " remotes") .. ". Select one to inspect its properties.") for i, inst in ipairs(hits) do if i > ROWS_CAP then break end flatRow(inst, classNameOf(inst)) end if #hits == 0 then noteLabel("Nothing matches that filter.") end end local function paintChips() for name, chip in pairs(chipButtons) do local on = WS.view == string.lower(name) chip.BackgroundTransparency = on and 0 or 1 chip.TextColor3 = on and C.text or C.subtext end collapseBtn.Visible = WS.view == "tree" filter.PlaceholderText = WS.view == "tree" and "Filter by name or class" or ("Filter " .. WS.view) end WS.render = function(keepScroll) local scrollY = list.CanvasPosition.Y for _, ch in ipairs(list:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end WS.order = 0 if WS.view == "scripts" then renderScripts() elseif WS.view == "remotes" then renderRemotes() else renderTree() end if keepScroll ~= false and scrollY > 0 then list.CanvasPosition = Vector2.new(0, scrollY) end end WS.renderTree = WS.render local function setView(name) WS.view = name paintChips() if root.Visible then WS.render(false) end end for name, chip in pairs(chipButtons) do chip.MouseButton1Click:Connect(function() setView(string.lower(name)) end) end collapseBtn.MouseButton1Click:Connect(function() WS.expanded = {} if WS.render then WS.render(false) end end) workspaceRefresh = function() if root.Visible and WS.render then WS.render() end end local navLabel for _, ch in ipairs(workspaceNav:GetChildren()) do if ch:IsA("TextLabel") then navLabel = ch end end local function setOpen(open) root.Visible = open if navLabel then navLabel.TextColor3 = open and C.text or C.subtext end if open then titleLabel.Text = "Workspace" subtitleLabel.Text = gameName .. " · " .. WS.view .. " explorer" paintChips() WS.render(false) else titleLabel.Text = current and current.title or "New chat" updateHeaderSubtitle() end end toggleWorkspace = function() setOpen(not root.Visible) end copySource.MouseButton1Click:Connect(function() if WS.source and copyToClipboard(WS.source) then copySource.Text = "Copied" task.delay(1.2, function() if copySource.Parent then copySource.Text = "Copy" end end) end end) filter:GetPropertyChangedSignal("Text"):Connect(function() if WS.render then WS.render(false) end end) refreshBtn.MouseButton1Click:Connect(function() WS.cache.items, WS.cache.at = nil, 0 if WS.render then WS.render() end end) closeWs.MouseButton1Click:Connect(function() setOpen(false) end) paintChips() task.defer(function() if workspacePanel and not workspacePanel.Visible then WS.render() end end) end local readConsole local CONSOLE do CONSOLE = { log = {}, max = 300, start = os.clock(), types = { [Enum.MessageType.MessageOutput] = "output", [Enum.MessageType.MessageInfo] = "info", [Enum.MessageType.MessageWarning] = "warning", [Enum.MessageType.MessageError] = "error", }, } pushLog = function(message, mtype, src) table.insert(CONSOLE.log, { t = os.clock() - CONSOLE.start, kind = CONSOLE.types[mtype] or "output", msg = tostring(message), src = src or "client" }) if #CONSOLE.log > CONSOLE.max then table.remove(CONSOLE.log, 1) end end pcall(function() for _, e in ipairs(LogService:GetLogHistory()) do pushLog(e.message, e.messageType) end end) LogService.MessageOut:Connect(function(message, mtype) pushLog(message, mtype) end) end function readConsole(args) args = args or {} local onlyErrors = args.filter ~= "all" local limit = math.clamp(tonumber(args.limit) or 40, 1, 100) local picked = {} for i = #CONSOLE.log, 1, -1 do local e = CONSOLE.log[i] if (not onlyErrors) or e.kind == "error" or e.kind == "warning" then table.insert(picked, 1, e) if #picked >= limit then break end end end local lines, total, errors, warns = {}, 0, 0, 0 for _, e in ipairs(picked) do if e.kind == "error" then errors += 1 elseif e.kind == "warning" then warns += 1 end local m = e.msg if #m > 400 then m = m:sub(1, 400) .. "...[truncated]" end local line = string.format("[+%.1fs][%s][%s] %s", e.t, e.src, e.kind, m) total += #line if total > 6000 then table.insert(lines, "...[more omitted]") break end table.insert(lines, line) end return (#lines > 0 and table.concat(lines, "\n") or "(no matching console entries)"), errors, warns end local captureGame, encodePNG do local crcTable = {} for i = 0, 255 do local c = i for _ = 1, 8 do if bit32.band(c, 1) == 1 then c = bit32.bxor(bit32.rshift(c, 1), 0xEDB88320) else c = bit32.rshift(c, 1) end end crcTable[i] = c end local function crc32buf(b, from, to) local c = 0xFFFFFFFF for i = from, to do c = bit32.bxor(crcTable[bit32.band(bit32.bxor(c, buffer.readu8(b, i)), 0xFF)], bit32.rshift(c, 8)) end return bit32.bxor(c, 0xFFFFFFFF) end local function writeU32BE(b, off, v) buffer.writeu8(b, off, bit32.band(bit32.rshift(v, 24), 255)) buffer.writeu8(b, off + 1, bit32.band(bit32.rshift(v, 16), 255)) buffer.writeu8(b, off + 2, bit32.band(bit32.rshift(v, 8), 255)) buffer.writeu8(b, off + 3, bit32.band(v, 255)) end local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" local B64c = {} for i = 1, 64 do B64c[i - 1] = B64:sub(i, i) end local function base64(b) local len = buffer.len(b) local out, n = {}, 0 for i = 0, len - 1, 3 do local b1 = buffer.readu8(b, i) local b2 = i + 1 < len and buffer.readu8(b, i + 1) or 0 local b3 = i + 2 < len and buffer.readu8(b, i + 2) or 0 local v = b1 * 65536 + b2 * 256 + b3 n += 1 out[n] = B64c[bit32.rshift(v, 18)] .. B64c[bit32.band(bit32.rshift(v, 12), 63)] .. (i + 1 < len and B64c[bit32.band(bit32.rshift(v, 6), 63)] or "=") .. (i + 2 < len and B64c[bit32.band(v, 63)] or "=") end return table.concat(out) end function encodePNG(img, targetW) local sw, sh = img.Size.X, img.Size.Y local tw = math.min(targetW, sw) local th = math.max(1, math.floor(sh * tw / sw + 0.5)) local src = img:ReadPixelsBuffer(Vector2.zero, Vector2.new(sw, sh)) local stride = 1 + tw * 3 local rawLen = stride * th local raw = buffer.create(rawLen) for y = 0, th - 1 do local sy = math.min(sh - 1, math.floor((y + 0.5) * sh / th)) local rowOff = y * stride buffer.writeu8(raw, rowOff, 0) for x = 0, tw - 1 do local sx = math.min(sw - 1, math.floor((x + 0.5) * sw / tw)) local si = (sy * sw + sx) * 4 local di = rowOff + 1 + x * 3 buffer.writeu8(raw, di, buffer.readu8(src, si)) buffer.writeu8(raw, di + 1, buffer.readu8(src, si + 1)) buffer.writeu8(raw, di + 2, buffer.readu8(src, si + 2)) end if y % 40 == 0 then task.wait() end end local a, bsum = 1, 0 for i = 0, rawLen - 1 do a = (a + buffer.readu8(raw, i)) % 65521 bsum = (bsum + a) % 65521 end local adler = bsum * 65536 + a local blocks = math.ceil(rawLen / 65535) local zLen = 2 + blocks * 5 + rawLen + 4 local out = buffer.create(8 + 25 + (12 + zLen) + 12) local sig = { 137, 80, 78, 71, 13, 10, 26, 10 } for i = 1, 8 do buffer.writeu8(out, i - 1, sig[i]) end local p = 8 writeU32BE(out, p, 13); p += 4 local ihdr = p buffer.writestring(out, p, "IHDR"); p += 4 writeU32BE(out, p, tw); p += 4 writeU32BE(out, p, th); p += 4 buffer.writeu8(out, p, 8); buffer.writeu8(out, p + 1, 2) buffer.writeu8(out, p + 2, 0); buffer.writeu8(out, p + 3, 0); buffer.writeu8(out, p + 4, 0) p += 5 writeU32BE(out, p, crc32buf(out, ihdr, p - 1)); p += 4 writeU32BE(out, p, zLen); p += 4 local idat = p buffer.writestring(out, p, "IDAT"); p += 4 buffer.writeu8(out, p, 0x78); buffer.writeu8(out, p + 1, 0x01); p += 2 local pos = 0 while pos < rawLen do local n = math.min(65535, rawLen - pos) local nn = bit32.band(bit32.bnot(n), 0xFFFF) buffer.writeu8(out, p, (pos + n >= rawLen) and 1 or 0) buffer.writeu8(out, p + 1, bit32.band(n, 255)); buffer.writeu8(out, p + 2, bit32.rshift(n, 8)) buffer.writeu8(out, p + 3, bit32.band(nn, 255)); buffer.writeu8(out, p + 4, bit32.rshift(nn, 8)) p += 5 buffer.copy(out, p, raw, pos, n) p += n pos += n end writeU32BE(out, p, adler); p += 4 writeU32BE(out, p, crc32buf(out, idat, p - 1)); p += 4 writeU32BE(out, p, 0); p += 4 local iend = p buffer.writestring(out, p, "IEND"); p += 4 writeU32BE(out, p, crc32buf(out, iend, p - 1)) return base64(out), tw, th end function captureGame() local function encodedPNG(data) if type(data) ~= "string" or #data < 64 then return nil end if data:sub(1, 3) == "iVB" then return data end if data:sub(2, 4) == "PNG" then return base64(buffer.fromstring(data)) end return nil end local names = { "getScreenshot", "takeScreenshot", "screenshot", "captureScreenshot", "getScreenCapture" } local function executorScreenshot() local okEnv, env = pcall(function() return (getgenv and getgenv()) or _G end) if not okEnv or type(env) ~= "table" then return nil end for _, name in ipairs(names) do local okFn, fn = pcall(function() return env[name] end) if okFn and type(fn) == "function" then local okOut, out = pcall(fn) local encoded = okOut and encodedPNG(out) if encoded then return encoded end if okOut and type(out) == "string" and type(readfile) == "function" then local okFile, data = pcall(readfile, out) if okFile then encoded = encodedPNG(data) if encoded then return encoded end end end end end return nil end gui.Enabled = false task.wait(0.15) local quick = executorScreenshot() if quick then gui.Enabled = true return { base64 = quick, mediaType = "image/png" } end local contentId, done = nil, false local okCap, errCap = pcall(function() CaptureService:CaptureScreenshot(function(id) contentId, done = id, true end) end) local t0 = os.clock() while okCap and not done and os.clock() - t0 < 5 do task.wait() end gui.Enabled = true if not okCap then return nil, tostring(errCap) end if not contentId then return nil, "screenshot timed out" end local okImg, img = pcall(function() return AssetService:CreateEditableImageAsync(Content.fromUri(contentId)) end) if not okImg then return nil, tostring(img) end return img end end local SCREENSHOT_WIDTH = 512 function captureAttachment() local img, err = captureGame() if not img then return nil, err end if img.base64 then return img end local ok, b64, w, h = pcall(encodePNG, img, SCREENSHOT_WIDTH) if not ok then return nil, tostring(b64) end return { editable = img, base64 = b64, mediaType = "image/png", w = w, h = h } end local TOOL_SCHEMAS = { { name = "read_console", access = "read", description = "Read recent Roblox console output (errors and warnings) from the running game. " .. "Use RARELY: only when the user reports a bug, crash or unexpected behavior, or when runtime output is " .. "truly needed to diagnose. Never for general questions. At most once per turn unless clearly insufficient.", parameters = { filter = "'errors' (default) or 'all'", limit = "max entries, default 40" }, setting = "console", }, { name = "take_screenshot", access = "read", description = "Capture what the player currently sees in the game. Use only when visual state matters " .. "(UI layout, rendering problems). Rarely needed.", parameters = {}, setting = "screen", }, { name = "inspect_player", access = "read", description = "Look at the player and the live game. view='avatar' (default) returns their character: rig type, " .. "body scales and colours, every accessory and clothing item, equipped tools, and attaches a picture " .. "of the avatar. view='scene' returns the place, the players in the server and their distance, " .. "your position, the camera, the time of day and the on-screen interfaces. Use this whenever the user " .. "asks about their avatar, outfit, appearance, character, health, position, or what is around them.", parameters = { view = "'avatar' (default) or 'scene'" }, }, { name = "inspect_instance", access = "read", description = "Inspect an instance in the game by path (e.g. 'game.Workspace' or 'Players.LocalPlayer'). " .. "Returns its class, key properties and children. Read only.", parameters = { path = "dotted path to the instance" }, }, { name = "read_script", access = "read", description = "Read the Source of a Script/LocalScript/ModuleScript by path. Returns the Luau source. " .. "May be locked for some scripts.", parameters = { path = "dotted path to the script" }, }, { name = "read_file", access = "read", description = "Read a text file from your executor workspace by path.", parameters = { path = "file path" }, }, { name = "web_search", access = "read", description = "Search the web and return the top results with titles, URLs and snippets. " .. "Use for current information, documentation, versions, prices, or anything you are unsure of. " .. "When your answer relies on results, cite sources as markdown links: [title](https://example.com/page).", parameters = { query = "search query" }, setting = "web", }, { name = "web_fetch", access = "read", description = "Fetch a web page by URL and return its readable text content. " .. "Use to read in full a source found via web_search or a URL the user gave you.", parameters = { url = "http(s) URL to read" }, setting = "web", }, { name = "set_property", access = "write", description = "Change a property on a game instance (e.g. Anchored, Transparency, Position). Modifies the live game.", parameters = { path = "dotted path to the instance", property = "property name", value = "new value" }, }, { name = "write_file", access = "write", description = "Write a text file to your executor workspace.", parameters = { path = "file path", content = "file contents" }, }, { name = "execute_luau", access = "execute", description = "Execute Luau code against the live game client and return printed output or errors. This runs real code.", parameters = { code = "Luau source to run" }, }, } function toolAccess(name) for _, t in ipairs(TOOL_SCHEMAS) do if t.name == name then return t.access or "read" end end return "read" end local SENSITIVE_FILES = { ["agentrouterrobloxai.json"] = true, [".env"] = true } local function isSensitivePath(path) local p = tostring(path or ""):lower():gsub("\\", "/") return SENSITIVE_FILES[p:match("([^/]+)$") or p] == true end local function redactSecrets(text) local s = tostring(text or "") for _, key in ipairs({ settings.apiKey, settings.opencodeApiKey, settings.openrouterKey, AGENTROUTER_KEY, OPENCODE_KEY }) do local k = tostring(key or ""):match("^%s*(.-)%s*$") if #k >= 12 then s = s:gsub(k:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1"), "[redacted]") end end return s end function permissionDecision(access) local p = settings.permission if p == "readonly" then return access == "read" and "run" or "deny" end -- A page the model just read is untrusted input. Nothing that changes state runs -- without an explicit yes for the rest of that turn. if access ~= "read" and webTainted then return "confirm" end if p == "ask" then return access == "read" and "run" or "confirm" elseif p == "auto" then return access == "execute" and "confirm" or "run" end return "run" end function findDeep(name, root, depth) depth = depth or 0 if depth > 7 then return nil end local ok, kids = pcall(function() return (root or workspace):GetChildren() end) if not ok or not kids then return nil end for _, ch in ipairs(kids) do if ch.Name:lower() == name:lower() then return ch end local f = findDeep(name, ch, depth + 1) if f then return f end end return nil end local function resolveInstance(path) if type(path) ~= "string" or path == "" then return nil end local cleaned = path:gsub(":", ".") local node = game for seg in cleaned:gmatch("[^%.]+") do if node == nil then return nil end if node == game and seg:lower() == "game" then node = game elseif node == game and seg:lower() == "workspace" then node = workspace else local nxt = nil if node == game then local ok, svc = pcall(function() return game:GetService(seg) end) if ok and svc ~= nil then nxt = svc end end if nxt == nil then local ok, ch = pcall(function() return node:FindFirstChild(seg) end) if ok then nxt = ch end end node = nxt end end if node == nil or node == game then node = findDeep((cleaned:gmatch("[^%.]+")() or cleaned)) or node end if node == game then return nil end return node end function confirmAction(toolName, access) local resolved, decision = false, false local overlay = new("Frame", { Name = "ConfirmOverlay", Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 0.5, ZIndex = 200 }, gui) local box = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(360, 176), BackgroundColor3 = C.panel, ZIndex = 201 }, overlay) round(box, 14) stroke(box) text({ Position = UDim2.fromOffset(18, 16), Size = UDim2.new(1, -36, 0, 24), Text = "Allow this action?", Font = Enum.Font.GothamBold, TextSize = 17, ZIndex = 202 }, box) text({ Position = UDim2.fromOffset(18, 48), Size = UDim2.new(1, -36, 0, 56), Text = tostring(toolName) .. "\nPermission level: " .. tostring(access) .. " · skips in 30s", TextSize = 13, TextColor3 = C.subtext, TextWrapped = true, ZIndex = 202 }, box) local denyBtn = button({ Position = UDim2.new(0, 18, 1, -54), Size = UDim2.new(0.5, -27, 0, 36), BackgroundColor3 = C.hover, Text = "Deny", TextSize = 14, ZIndex = 202 }, box) round(denyBtn, 9) local allowBtn = button({ Position = UDim2.new(0.5, 9, 1, -54), Size = UDim2.new(0.5, -27, 0, 36), BackgroundColor3 = C.error, Text = "Allow", TextSize = 14, ZIndex = 202 }, box) round(allowBtn, 9) local countdown = text({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -18, 0, 14), Size = UDim2.fromOffset(48, 20), Text = "30s", TextSize = 12, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false, ZIndex = 202 }, box) denyBtn.MouseButton1Click:Connect(function() decision = false resolved = true end) allowBtn.MouseButton1Click:Connect(function() decision = true resolved = true end) local t0 = os.clock() while not resolved and overlay.Parent and os.clock() - t0 < 30 do countdown.Text = tostring(math.max(0, math.ceil(30 - (os.clock() - t0)))) .. "s" task.wait(0.05) end overlay:Destroy() if not resolved then return "timeout" end return decision and "allow" or "deny" end function activeToolSchemas() local list = {} for _, t in ipairs(TOOL_SCHEMAS) do local shown = true if t.setting ~= nil then shown = settings[t.setting] ~= false end if shown and not (settings.permission == "readonly" and (t.access or "read") ~= "read") then table.insert(list, t) end end return list end local webSearch, webFetch function runTool(call, approved) local access = toolAccess(call.name) if not approved then local decision = permissionDecision(access) if decision == "deny" then return "Blocked by the Read only permission mode.", "Blocked (read only)", "denied" end if decision == "confirm" then local answer = confirmAction(call.name, access) if answer == "timeout" then return "The approval prompt timed out after 30 seconds, so the action was skipped.", "Timed out", "denied" elseif answer ~= "allow" then return "The user declined this action.", "Declined", "denied" end end end if call.name == "read_console" then if not settings.console then return "Console access is turned off by the user.", "Console access is off", "denied" end local out, e, w = readConsole(call.args) return out, string.format("Read console: %d error%s, %d warning%s", e, e == 1 and "" or "s", w, w == 1 and "" or "s"), "done" elseif call.name == "take_screenshot" then if not settings.screen then return "Game screenshots are turned off by the user.", "Game vision is off", "denied" end local att, err = captureAttachment() if not att then return "Screenshot failed: " .. tostring(err) .. "\nThis executor blocks screen capture, so no image is available. Use inspect_player instead " .. "(it reads the avatar and the scene and can attach the avatar picture), or ask the user to " .. "attach a screenshot in the composer.", "Screenshot blocked", "error" end return "Screenshot attached.", "Took a screenshot", "done", att elseif call.name == "inspect_player" then local view = tostring((call.args and call.args.view) or "avatar"):lower() if view ~= "scene" then view = "avatar" end local report = API.playerReport(view) if view == "avatar" then local image = API.avatarImage() if image then return report .. "\nAvatar picture attached.", "Looked at your avatar", "done", image end end return report, view == "scene" and "Looked around the game" or "Read your avatar", "done" elseif call.name == "inspect_instance" then local target = resolveInstance((call.args and call.args.path) or "") if not target then return "Instance not found.", "Not found", "error" end local lines = { target.ClassName .. " " .. target:GetFullName() } for _, p in ipairs({ "Position", "Size", "Transparency", "Anchored", "Enabled", "Value", "ClassName" }) do local ok, v = pcall(function() return target[p] end) if ok and v ~= nil and type(v) ~= "userdata" then lines[#lines + 1] = p .. " = " .. tostring(v) end if ok and typeof(v) == "Vector3" then lines[#lines + 1] = p .. " = " .. tostring(v) end end local kids = {} pcall(function() kids = target:GetChildren() end) lines[#lines + 1] = "Children (" .. #kids .. "):" for i, ch in ipairs(kids) do if i > 40 then lines[#lines + 1] = " ... +" .. (#kids - 40) .. " more"; break end lines[#lines + 1] = " " .. ch.Name .. " (" .. ch.ClassName .. ")" end return table.concat(lines, "\n"), "Inspected " .. target.Name, "done" elseif call.name == "read_script" then local target = resolveInstance((call.args and call.args.path) or "") if not target then return "Script not found.", "Not found", "error" end local ok, src = API.withIdentity(function() return pcall(function() return target.Source end) end) if not ok or type(src) ~= "string" then return "Could not read Source (locked or not a script).", "Read blocked", "error" end return src, "Read " .. target.Name, "done" elseif call.name == "read_file" then local path = tostring((call.args and call.args.path) or "") if path == "" then return "No path given.", "No path", "error" end if isSensitivePath(path) then return "That file holds credentials and is not readable by tools.", "Blocked (credentials)", "denied" end if type(readfile) ~= "function" then return "File API unavailable in this executor.", "No file API", "error" end local ok, data = pcall(function() return readfile(path) end) if not ok then return "File not found or unreadable: " .. path, "Read failed", "error" end return tostring(data), "Read " .. path, "done" elseif call.name == "web_search" then if not settings.web then return "Web search is turned off by the user.", "Web search is off", "denied" end local q = tostring((call.args and call.args.query) or ""):gsub("^%s+", ""):gsub("%s+$", "") if q == "" then return "No search query given.", "No query", "error" end local out, err = webSearch(q) if not out then return "Search failed: " .. tostring(err), "Search failed", "error" end webTainted = true return out, "Searched the web: " .. (#q > 36 and q:sub(1, 33) .. "..." or q), "done" elseif call.name == "web_fetch" then if not settings.web then return "Web access is turned off by the user.", "Web access is off", "denied" end local url = tostring((call.args and call.args.url) or ""):gsub("^%s+", ""):gsub("%s+$", "") if url == "" then return "No URL given.", "No URL", "error" end local out, err = webFetch(url) if not out then return "Could not read the page: " .. tostring(err), "Fetch failed", "error" end webTainted = true return out, "Read " .. (url:match("^https?://([^/]+)") or url), "done" elseif call.name == "set_property" then local args = call.args or {} local target = resolveInstance(args.path or "") if not target then return "Instance not found.", "Not found", "error" end local prop = tostring(args.property or "") if prop == "" then return "No property named.", "No property", "error" end local val = args.value local okGet, cur = pcall(function() return target[prop] end) if okGet and typeof(cur) == "Vector3" and type(val) == "table" then val = Vector3.new(val[1] or val.x or 0, val[2] or val.y or 0, val[3] or val.z or 0) end local ok, err = API.withIdentity(function() return pcall(function() target[prop] = val end) end) if not ok then return "Failed to set " .. prop .. ": " .. tostring(err), "Set failed", "error" end return "Set " .. prop .. " = " .. tostring(val), "Set " .. prop, "done" elseif call.name == "write_file" then local args = call.args or {} local path = tostring(args.path or "") if path == "" then return "No path given.", "No path", "error" end if isSensitivePath(path) then return "That file holds credentials and is not writable by tools.", "Blocked (credentials)", "denied" end if type(writefile) ~= "function" then return "File API unavailable in this executor.", "No file API", "error" end local content = tostring(args.content or "") local ok, err = pcall(function() writefile(path, content) end) if not ok then return "Write failed: " .. tostring(err), "Write failed", "error" end return "Wrote " .. #content .. " bytes to " .. path, "Wrote " .. path, "done" elseif call.name == "execute_luau" then local code = tostring((call.args and call.args.code) or "") if code == "" then return "No code provided.", "No code", "error" end if type(loadstring) ~= "function" then return "This executor does not provide loadstring, so code cannot run here.", "No loadstring", "error" end local out = {} local function push(...) local t = {} for i = 1, select("#", ...) do t[#t + 1] = tostring(select(i, ...)) end out[#out + 1] = table.concat(t, " ") end local okLoad, fn, cerr = pcall(loadstring, code) if not okLoad then return "Compile error: " .. tostring(fn), "Compile error", "error" end if not fn then return "Compile error: " .. tostring(cerr), "Compile error", "error" end local env = setmetatable({ print = push, warn = push, write = push }, { __index = _G }) if type(setfenv) == "function" then pcall(setfenv, fn, env) end local okRun, rerr = API.withIdentity(function() return pcall(fn) end) local output = table.concat(out, "\n") if not okRun then local msg = "Runtime error: " .. tostring(rerr) if output ~= "" then msg = output .. "\n" .. msg end return msg, "Execution error", "error" end return output ~= "" and output or "Executed without output.", "Executed Luau", "done" end if call.name == "bash" or call.name == "edit" or call.name == "glob" or call.name == "grep" or call.name == "read" then return "Shell and editor tools are not available here. Use inspect_instance, read_script, read_file, write_file, set_property or execute_luau instead.", "Tool unavailable", "denied" end return "Unknown tool.", "Unknown tool: " .. tostring(call.name), "error" end function parseSegments(src) local segs, pos = {}, 1 while true do local s, e = src:find("```", pos, true) if not s then table.insert(segs, { kind = "text", text = src:sub(pos) }) break end table.insert(segs, { kind = "text", text = src:sub(pos, s - 1) }) local close, closeE = src:find("```", e + 1, true) local raw if close then raw = src:sub(e + 1, close - 1) pos = closeE + 1 else raw = src:sub(e + 1) pos = #src + 1 end local lang, body = raw:match("^([%w_%-]*)\n(.*)$") if not lang then lang, body = "", raw end table.insert(segs, { kind = "code", lang = lang, text = (body:gsub("\n$", "")) }) if not close then break end end return segs end codeHex = Color3.fromRGB(240, 165, 135):ToHex() linkHex = Color3.fromRGB(96, 146, 245):ToHex() function richify(s) s = s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">") s = s:gsub("`([^`\n]+)`", '%1') s = s:gsub("%*%*(.-)%*%*", "%1") s = s:gsub("%[([^%[%]]+)%]%((https?://[^%s%)]+)%)", '%1') s = s:gsub("https?://[%w%-%._~:/%?#%[%]@!$&'()*+,;=%%]+", function(u) local clean = u:gsub("[%.%,%;%!%)%]]+$", "") return '' .. clean .. "" .. u:sub(#clean + 1) end) local lines = {} for line in (s .. "\n"):gmatch("(.-)\n") do line = line:gsub("^#+%s+(.*)$", "%1") line = line:gsub("^%s*[%-%*]%s+", "• ") table.insert(lines, line) end return table.concat(lines, "\n") end function renderContent(container, str) for _, ch in ipairs(container:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end local order = 0 for _, seg in ipairs(parseSegments(str)) do if seg.kind == "text" then local t = seg.text:match("^%s*(.-)%s*$") if t ~= "" then order += 1 text({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, RichText = true, Text = richify(t), TextSize = 15, LineHeight = 1.3, LayoutOrder = order, }, container) end else order += 1 local lineCount = 1 for _ in string.gmatch(seg.text, "\n") do lineCount = lineCount + 1 end if lineCount > 50 then local fileBtn = button({ Size = UDim2.new(1, 0, 0, 52), BackgroundColor3 = C.panel, LayoutOrder = order }, container) round(fileBtn, 10) stroke(fileBtn) hover(fileBtn, C.hover, 0) local iconHolder = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(12, 12), Size = UDim2.fromOffset(28, 28) }, fileBtn) icon("file", iconHolder, C.text, 24) text({ Position = UDim2.fromOffset(52, 8), Size = UDim2.new(1, -64, 0, 20), Text = (seg.lang ~= "" and seg.lang or "txt") .. " file", Font = Enum.Font.GothamMedium, TextSize = 14, TextWrapped = false }, fileBtn) text({ Position = UDim2.fromOffset(52, 26), Size = UDim2.new(1, -64, 0, 16), Text = tostring(lineCount) .. " lines · Click to view", TextSize = 12, TextColor3 = C.subtext, TextWrapped = false }, fileBtn) fileBtn.MouseButton1Click:Connect(function() copyToClipboard(seg.text) end) else local box = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundColor3 = C.code, LayoutOrder = order }, container) round(box, 10) stroke(box) new("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder }, box) local head = new("Frame", { Size = UDim2.new(1, 0, 0, 34), BackgroundTransparency = 1, LayoutOrder = 1 }, box) local dot = new("Frame", { Position = UDim2.fromOffset(14, 13), Size = UDim2.fromOffset(8, 8), BackgroundColor3 = Color3.fromRGB(240, 165, 135), BorderSizePixel = 0 }, head) round(dot, 4) local langName = (seg.lang ~= "" and seg.lang or "code"):lower() text({ Position = UDim2.fromOffset(29, 0), Size = UDim2.new(1, -150, 1, 0), Text = langName, TextSize = 11, TextColor3 = C.subtext, Font = Enum.Font.GothamBold, TextWrapped = false }, head) local copyBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(56, 24), BackgroundColor3 = C.hover, Text = "Copy", TextSize = 11 }, head) round(copyBtn, 6) hover(copyBtn, C.panel, 0) copyBtn.MouseButton1Click:Connect(function() if copyToClipboard(seg.text) then copyBtn.Text = "Copied" task.delay(1.2, function() if copyBtn.Parent then copyBtn.Text = "Copy" end end) end end) new("Frame", { Position = UDim2.new(0, 0, 1, -1), Size = UDim2.new(1, 0, 0, 1), BackgroundColor3 = C.stroke, BorderSizePixel = 0 }, head) local body = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 2 }, box) pad(body, 10, 14, 14, 14) text({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, Text = seg.text, Font = Enum.Font.Code, TextSize = 13, TextColor3 = Color3.fromRGB(232, 228, 218) }, body) end end end end function scrollToBottom() task.defer(function() RunService.Heartbeat:Wait() messageArea.CanvasPosition = Vector2.new(0, math.max(0, messageArea.AbsoluteCanvasSize.Y - messageArea.AbsoluteSize.Y)) end) end local updateComposer, generate, send, refreshSidebar, refreshAttachStrip, attachScreenshot, refreshProjects function drawToolChip(parent, tl, order) local chip = new("Frame", { Size = UDim2.fromOffset(0, 28), AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.panel, LayoutOrder = order }, parent) round(chip, 14) stroke(chip) pad(chip, 0, 12, 0, 0) local kindMap = { web = "globe", web_search = "globe", web_fetch = "globe", note = "check", read_console = "terminal", take_screenshot = "image", inspect_instance = "search", read_script = "file", read_file = "file", write_file = "file", set_property = "settings", execute_luau = "terminal", task = "terminal" } local kind = kindMap[tl.kind] or "terminal" local col = tl.state == "error" and C.error or C.subtext local holder = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(9, 6), Size = UDim2.fromOffset(16, 16) }, chip) icon(kind, holder, col, 16) local label = tl.label if #label > 64 then label = label:sub(1, 61) .. "..." end if tl.state == "running" then label = label .. "..." end text({ Position = UDim2.fromOffset(32, 0), Size = UDim2.new(0, 0, 1, 0), AutomaticSize = Enum.AutomaticSize.X, Text = label, TextSize = 12, TextColor3 = col, TextWrapped = false }, chip) end function drawUser(msg, index) local r = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = index }, column) new("UIListLayout", { HorizontalAlignment = Enum.HorizontalAlignment.Right, Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, r) text({ Size = UDim2.fromOffset(0, 16), AutomaticSize = Enum.AutomaticSize.X, Text = player.DisplayName, TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 0 }, r) if msg.images and #msg.images > 0 then local imgs = new("Frame", { Size = UDim2.fromOffset(0, 68), AutomaticSize = Enum.AutomaticSize.X, BackgroundTransparency = 1, LayoutOrder = 1 }, r) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, imgs) for i, att in ipairs(msg.images) do local il = new("ImageLabel", { Size = UDim2.fromOffset(120, 68), BackgroundColor3 = C.userBubble, ScaleType = Enum.ScaleType.Crop, LayoutOrder = i }, imgs) round(il, 10) stroke(il) pcall(function() il.ImageContent = Content.fromObject(att.editable) end) end end if msg.text ~= "" then local bubble = new("Frame", { AutomaticSize = Enum.AutomaticSize.XY, Size = UDim2.fromOffset(0, 0), BackgroundColor3 = C.userBubble, LayoutOrder = 2 }, r) round(bubble, 16) stroke(bubble, C.stroke) pad(bubble, 10, 16, 10, 16) local colW = column.AbsoluteSize.X local lbl = text({ AutomaticSize = Enum.AutomaticSize.XY, Size = UDim2.fromOffset(0, 0), Text = msg.text, TextSize = 15, LineHeight = 1.25 }, bubble) new("UISizeConstraint", { MaxSize = Vector2.new(colW > 0 and math.min(480, colW * 0.8) or 420, math.huge) }, lbl) end end function formatTime(seconds) if seconds >= 60 then local mins = math.floor(seconds / 60) local secs = seconds % 60 return string.format("%dm %02ds", mins, secs) else return string.format("%ds", seconds) end end function extractSources(str) local seen, list = {}, {} local function add(url, title) url = url:gsub("[%.%,%;%!%)%]]+$", "") if url:match("^https?://") and not seen[url] and #list < 10 then seen[url] = true list[#list + 1] = { url = url, title = title or (url:match("^https?://([^/]+)") or url) } end end for title, url in str:gmatch("%[([^%[%]]+)%]%((https?://[^%s%)]+)%)") do add(url, (title:gsub("^%s+", ""):gsub("%s+$", ""))) end for url in str:gmatch("https?://[%w%-%._~:/%?#%[%]@!$&'()*+,;=%%]+") do add(url) end return list end function drawSourceRow(parent, src, order) local row = button({ Size = UDim2.new(1, 0, 0, 40), BackgroundColor3 = C.panel, LayoutOrder = order }, parent) round(row, 9) stroke(row) hover(row, C.hover, 0) local ih = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(11, 12), Size = UDim2.fromOffset(16, 16) }, row) icon("globe", ih, C.subtext, 15) text({ Position = UDim2.fromOffset(36, 5), Size = UDim2.new(1, -110, 0, 17), Text = src.title, TextSize = 12, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, row) local host = src.url:match("^https?://([^/]+)") or src.url text({ Position = UDim2.fromOffset(36, 22), Size = UDim2.new(1, -110, 0, 15), Text = host, TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, row) local hint = text({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -12, 0.5, 0), Size = UDim2.fromOffset(52, 16), Text = "Copy", TextSize = 11, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false }, row) row.MouseButton1Click:Connect(function() if copyToClipboard(src.url) then hint.Text = "Copied" task.delay(1.2, function() if row.Parent then hint.Text = "Copy" end end) end end) end function drawAssistant(msg, index, isLast) local r = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = index }, column) local logoHolder = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(26, 26) }, r) local answering = modelById(msg.answeredById) or msg.model local logo = brandLogo(logoHolder, answering, 22) logo.Position = UDim2.fromOffset(2, 2) local who = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(38, 1), Size = UDim2.new(1, -38, 0, 22) }, r) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6), VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, who) text({ Size = UDim2.fromOffset(0, 20), AutomaticSize = Enum.AutomaticSize.X, Text = "Assistant", Font = Enum.Font.GothamBold, TextSize = 13, TextWrapped = false, LayoutOrder = 1 }, who) local whoLabel = answering.short or answering.name if msg.answeredById and msg.fellBackReason and answering ~= msg.model then whoLabel = whoLabel .. " · for " .. (msg.model.short or msg.model.name) end text({ Size = UDim2.fromOffset(0, 20), AutomaticSize = Enum.AutomaticSize.X, Text = whoLabel, TextSize = 12, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 2 }, who) local content = new("Frame", { Position = UDim2.fromOffset(38, 26), Size = UDim2.new(1, -38, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1 }, r) new("UIListLayout", { Padding = UDim.new(0, 10), SortOrder = Enum.SortOrder.LayoutOrder }, content) local think = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 1, Visible = false }, content) new("UIListLayout", { Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, think) local thinkHead = button({ Size = UDim2.fromOffset(0, 24), AutomaticSize = Enum.AutomaticSize.X, LayoutOrder = 1 }, think) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6), VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, thinkHead) local thinkTitle = text({ Size = UDim2.fromOffset(0, 20), AutomaticSize = Enum.AutomaticSize.X, Text = "Thinking...", TextSize = 13, TextColor3 = C.subtext, Font = Enum.Font.GothamMedium, TextWrapped = false, LayoutOrder = 1 }, thinkHead) local chevHolder = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(14, 14), LayoutOrder = 2 }, thinkHead) local chevIcon = icon("chevron", chevHolder, C.subtext, 14) local thinkBody = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 2 }, think) new("Frame", { Size = UDim2.new(0, 2, 1, 0), BackgroundColor3 = C.stroke, BorderSizePixel = 0 }, thinkBody) local thinkText = text({ Position = UDim2.fromOffset(14, 0), Size = UDim2.new(1, -14, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, Text = "", TextSize = 13, TextColor3 = C.subtext, LineHeight = 1.25, RichText = true }, thinkBody) thinkHead.MouseButton1Click:Connect(function() msg.thinkOpen = not msg.thinkOpen if msg._update then msg._update() end end) local taskF = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 2, Visible = false }, content) new("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }, taskF) local toolsF = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 3, Visible = false }, content) new("UIListLayout", { Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, toolsF) local toolSig = "" local dots = text({ Size = UDim2.new(1, 0, 0, 20), Text = "Thinking", TextSize = 14, TextColor3 = C.subtext, LayoutOrder = 4, Visible = false }, content) local bodyC = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 5 }, content) new("UIListLayout", { Padding = UDim.new(0, 12), SortOrder = Enum.SortOrder.LayoutOrder }, bodyC) local bodyStroke = stroke(bodyC, C.error) bodyStroke.Transparency = 1 local bodyPad = pad(bodyC, 0, 0, 0, 0) local sourcesF = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 6, Visible = false }, content) new("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }, sourcesF) local actions = new("Frame", { Size = UDim2.new(1, 0, 0, 24), BackgroundTransparency = 1, LayoutOrder = 7, Visible = false }, content) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 12), VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, actions) local retry = button({ Size = UDim2.fromOffset(0, 24), AutomaticSize = Enum.AutomaticSize.X, Text = "Retry", TextSize = 12, TextColor3 = C.subtext, LayoutOrder = 1 }, actions) retry.MouseEnter:Connect(function() retry.TextColor3 = C.text end) retry.MouseLeave:Connect(function() retry.TextColor3 = C.subtext end) retry.MouseButton1Click:Connect(function() if busy then return end table.remove(current.messages) generate() end) text({ Size = UDim2.fromOffset(0, 24), AutomaticSize = Enum.AutomaticSize.X, Text = answering.name, TextSize = 12, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 2 }, actions) local lastText = nil local lastSources = nil local function updateAssistantMessage() if not r.Parent then return end local waiting = (msg.status == "thinking" or msg.status == "reasoning" or msg.status == "planning" or msg.status == "editing" or msg.status == "testing") local elapsed = math.floor(os.clock() - (msg.startClock or os.clock())) local timeStr = formatTime(elapsed) local doneTimeStr = formatTime(msg.thinkSecs or 0) think.Visible = msg.thinkOn == true or (msg.thinking and msg.thinking ~= "") thinkTitle.Text = waiting and ((msg.taskPhase or "Thinking") .. "... (" .. timeStr .. ")") or ("Thought for " .. doneTimeStr) thinkBody.Visible = msg.thinkOpen == true thinkText.Text = richify(msg.thinking or "") chevIcon.Rotation = msg.thinkOpen and 0 or -90 if msg.taskPhase and msg.status ~= "done" then taskF.Visible = true for _, ch in ipairs(taskF:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end local phaseChip = new("Frame", { Size = UDim2.fromOffset(0, 26), AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.panel, LayoutOrder = 1 }, taskF) round(phaseChip, 13) stroke(phaseChip) pad(phaseChip, 0, 12, 0, 10) text({ Size = UDim2.new(0, 0, 1, 0), AutomaticSize = Enum.AutomaticSize.X, Text = "Task: " .. tostring(msg.taskPhase), TextSize = 12, TextColor3 = MODELS[modelIndex].accent, TextWrapped = false }, phaseChip) else taskF.Visible = false end local sig = "" for _, tl in ipairs(msg.tools or {}) do sig ..= tl.state .. tl.label .. "|" end if sig ~= toolSig then toolSig = sig for _, ch in ipairs(toolsF:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end for i, tl in ipairs(msg.tools or {}) do drawToolChip(toolsF, tl, i) end end toolsF.Visible = msg.tools ~= nil and #msg.tools > 0 dots.Visible = (msg.status == "thinking" or msg.status == "planning") and not msg.thinkOn dots.Text = (msg.taskPhase or "Thinking") .. string.rep(".", msg.dots or 0) if msg.text ~= lastText then lastText = msg.text renderContent(bodyC, msg.text) if msg.isError then for _, d in ipairs(bodyC:GetDescendants()) do if d:IsA("TextLabel") then d.TextColor3 = C.error end end end end local srcKey = (msg.status == "done" and not msg.isError) and msg.text or nil if srcKey ~= lastSources then lastSources = srcKey for _, ch in ipairs(sourcesF:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end local srcs = srcKey and extractSources(srcKey) or {} if #srcs > 0 then text({ Size = UDim2.new(1, 0, 0, 16), Text = "Sources", Font = Enum.Font.GothamBold, TextSize = 12, TextColor3 = C.subtext, LayoutOrder = 1 }, sourcesF) for i, s in ipairs(srcs) do drawSourceRow(sourcesF, s, i + 1) end end sourcesF.Visible = #srcs > 0 end if msg.isError then bodyC.BackgroundColor3 = C.error bodyC.BackgroundTransparency = 0.88 bodyStroke.Transparency = 0.55 bodyPad.PaddingTop = UDim.new(0, 10) bodyPad.PaddingBottom = UDim.new(0, 10) bodyPad.PaddingLeft = UDim.new(0, 12) bodyPad.PaddingRight = UDim.new(0, 12) else bodyC.BackgroundTransparency = 1 bodyStroke.Transparency = 1 bodyPad.PaddingTop = UDim.new(0, 0) bodyPad.PaddingBottom = UDim.new(0, 0) bodyPad.PaddingLeft = UDim.new(0, 0) bodyPad.PaddingRight = UDim.new(0, 0) end bodyC.Visible = msg.text ~= "" actions.Visible = msg.status == "done" and isLast end msg._update = updateAssistantMessage updateAssistantMessage() end function drawWelcome() local h = math.max(300, messageArea.AbsoluteSize.Y - 40) local wrap = new("Frame", { Size = UDim2.new(1, 0, 0, h), BackgroundTransparency = 1 }, column) local mid = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1 }, wrap) new("UIListLayout", { Padding = UDim.new(0, 16), SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Center }, mid) local m = MODELS[modelIndex] local logoModel = settings.autoLogo == false and MODELS[1] or m local logoHolder = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(56, 56), LayoutOrder = 1 }, mid) brandLogo(logoHolder, logoModel, 52).Position = UDim2.fromOffset(2, 2) text({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, Text = "What will we create, " .. player.DisplayName .. "?", Font = SERIF, TextSize = small and 22 or 30, TextXAlignment = Enum.TextXAlignment.Center, LayoutOrder = 2 }, mid) text({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, Text = "Your ideas. Your game. An agent to help make it happen.", TextSize = 13, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Center, LayoutOrder = 3 }, mid) local grid = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = 4 }, mid) new("UIGridLayout", { CellSize = UDim2.new(0.5, -6, 0, 92), CellPadding = UDim2.fromOffset(12, 12), SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Center }, grid) local cards = { { icon = "search", title = "Explore this game", desc = "Find your bearings in the world.", prompt = "Give me a quick overview of this game - what is here and what can I do?" }, { icon = "code", title = "Create something", desc = "Turn an idea into a working script.", prompt = "Help me turn an idea into a working Luau script." }, { icon = "settings", title = "Check performance", desc = "Understand FPS, memory, and latency.", prompt = "Help me check FPS, memory, and latency in this game." }, { icon = "person", title = "Inspect my character", desc = "See your character's current state.", prompt = "Inspect my character's current state and stats." }, } for i, c in ipairs(cards) do local card = button({ BackgroundColor3 = C.panel, LayoutOrder = i, Text = "", TextXAlignment = Enum.TextXAlignment.Left }, grid) round(card, 12) stroke(card) hover(card, C.hover, 0) local ih = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(16, 14), Size = UDim2.fromOffset(24, 24) }, card) icon(c.icon, ih, m.accent, 22) text({ Position = UDim2.fromOffset(16, 46), Size = UDim2.new(1, -32, 0, 20), Text = c.title, Font = Enum.Font.GothamMedium, TextSize = 15, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, card) text({ Position = UDim2.fromOffset(16, 66), Size = UDim2.new(1, -32, 0, 18), Text = c.desc, TextSize = 12, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, card) card.MouseButton1Click:Connect(function() send(c.prompt) end) end end local function renderMessages() for _, ch in ipairs(column:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end for _, m in ipairs(current.messages) do m._update = nil end titleLabel.Text = current.title updateModelChip() updateHeaderSubtitle() if #current.messages == 0 then drawWelcome() else for i, m in ipairs(current.messages) do if m.role == "user" then drawUser(m, i) else drawAssistant(m, i, i == #current.messages) end end end scrollToBottom() end local function setSidebar(open, instant) sidebarOpen = open sideInner.Size = UDim2.new(0, SIDEBAR_W, 1, 0) local dur = instant and 0 or 0.22 local function apply(inst, props) if instant or dur == 0 then for k, v in pairs(props) do inst[k] = v end else tween(inst, dur, props) end end if small then apply(sidebar, { Position = UDim2.fromOffset(open and 0 or -SIDEBAR_W - 8, 0), Size = UDim2.new(0, SIDEBAR_W, 1, 0) }) apply(main, { Position = UDim2.fromOffset(0, 0), Size = UDim2.new(1, 0, 1, 0) }) if not open then tween(sidebar, dur, { BackgroundTransparency = 0.15 }) else sidebar.BackgroundTransparency = 0 end else apply(sidebar, { Size = UDim2.new(0, open and SIDEBAR_W or 0, 1, 0) }) apply(main, { Position = UDim2.fromOffset(open and SIDEBAR_W or 0, 0), Size = UDim2.new(1, open and -SIDEBAR_W or 0, 1, 0) }) end end sideCloseBtn.MouseButton1Click:Connect(function() setSidebar(false) end) local function selectConversation(convo) current = convo renderMessages() refreshSidebar() if small then setSidebar(false) end end local function selectProject(proj) currentProject = proj conversations = proj.conversations proj.expanded = true updateHeaderSubtitle() refreshSidebar() end local function newConversation(forceNew) local list = currentProject and currentProject.conversations or conversations if not forceNew and current and #current.messages == 0 then selectConversation(current) return end local convo = { title = "New chat", messages = {} } table.insert(list, 1, convo) selectConversation(convo) end local function newProject(name, context) local p = { name = name or ("Project " .. tostring(#projects + 1)), conversations = {}, context = context or "", expanded = true } table.insert(projects, 1, p) return p end function newFolder() local p = newProject("Folder " .. tostring(#projects + 1)) selectProject(p) newConversation(true) refreshSidebar() end -- Context Menu Logic -- local contextMenu local function hideContextMenu() if contextMenu then contextMenu:Destroy() contextMenu = nil end end function showContextMenu(x, y, items) hideContextMenu() local h = #items * 32 + 8 contextMenu = new("Frame", { Position = UDim2.fromOffset(x, y), Size = UDim2.fromOffset(150, h), BackgroundColor3 = C.panel, ZIndex = 100 }, gui) round(contextMenu, 8) stroke(contextMenu) new("UIListLayout", { Padding = UDim.new(0, 2), SortOrder = Enum.SortOrder.LayoutOrder }, contextMenu) new("UIPadding", { PaddingTop = UDim.new(0, 4), PaddingBottom = UDim.new(0, 4) }, contextMenu) for i, item in ipairs(items) do local btn = button({ Size = UDim2.new(1, 0, 0, 30), BackgroundTransparency = 1, TextXAlignment = Enum.TextXAlignment.Left, Text = item.text, TextColor3 = item.color or C.text, TextSize = 13, ZIndex = 101, LayoutOrder = i }, contextMenu) pad(btn, 0, 0, 0, 10) hover(btn, C.hover, 0) btn.MouseButton1Click:Connect(function() hideContextMenu() if item.callback then item.callback() end end) end local catcher = new("TextButton", { Size = UDim2.fromScale(1,1), BackgroundTransparency = 1, ZIndex = 99, Text = "" }, gui) catcher.MouseButton1Click:Connect(hideContextMenu) task.delay(0.1, function() if catcher then catcher:Destroy() end end) end local renameUI = {} function showRenamePrompt(initialText, callback) if renameUI.overlay then renameUI.overlay:Destroy() end renameUI.overlay = new("Frame", { Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 0.5, ZIndex = 150 }, gui) local box = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(300, 120), BackgroundColor3 = C.panel, ZIndex = 151 }, renameUI.overlay) round(box, 12) stroke(box) text({ Position = UDim2.fromOffset(16, 12), Size = UDim2.new(1, -32, 0, 20), Text = "Rename", Font = Enum.Font.GothamBold, TextSize = 16, ZIndex = 152 }, box) renameUI.box = new("TextBox", { Position = UDim2.fromOffset(16, 40), Size = UDim2.new(1, -32, 0, 32), BackgroundColor3 = C.bg, Text = initialText, TextColor3 = C.text, TextSize = 13, Font = Enum.Font.Gotham, ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 152 }, box) round(renameUI.box, 6); stroke(renameUI.box, C.stroke); pad(renameUI.box, 0, 10, 0, 10) local okBtn = button({ Position = UDim2.fromOffset(16, 80), Size = UDim2.new(0.5, -20, 0, 28), BackgroundColor3 = MODELS[modelIndex].accent, Text = "Save", TextSize = 13, ZIndex = 152 }, box) round(okBtn, 6) local cancelBtn = button({ Position = UDim2.new(0.5, 4, 0, 80), Size = UDim2.new(0.5, -20, 0, 28), BackgroundColor3 = C.hover, Text = "Cancel", TextSize = 13, ZIndex = 152 }, box) round(cancelBtn, 6) okBtn.MouseButton1Click:Connect(function() callback(renameUI.box.Text) renameUI.overlay:Destroy() renameUI.overlay = nil end) cancelBtn.MouseButton1Click:Connect(function() renameUI.overlay:Destroy() renameUI.overlay = nil end) end function refreshSidebar() for _, ch in ipairs(convoList:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end local q = searchBox.Text:lower() local order = 0 function sideHeader(label, onClick) order = order + 1 local row = button({ Size = UDim2.new(1, 0, 0, 26), LayoutOrder = order }, convoList) round(row, 7) hover(row, C.hover, 0.6) text({ Position = UDim2.fromOffset(10, 0), Size = UDim2.new(1, -44, 1, 0), Text = label, TextSize = 12, Font = Enum.Font.GothamBold, TextColor3 = C.subtext, TextWrapped = false }, row) local add = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -4, 0.5, 0), Size = UDim2.fromOffset(22, 22) }, row) round(add, 6) icon("plus", add, C.subtext, 13) hover(add, C.hover, 0.4) add.MouseButton1Click:Connect(onClick) end sideHeader("Folders", function() if newFolder then newFolder() end end) sideHeader("Chats", function() newConversation(true) end) for _, proj in ipairs(projects) do local hasMatching = q == "" or proj.name:lower():find(q, 1, true) if not hasMatching then for _, c in ipairs(proj.conversations) do if c.title:lower():find(q, 1, true) then hasMatching = true break end end end if hasMatching then order = order + 1 -- Folder Header local header = button({ Size = UDim2.new(1, 0, 0, 32), BackgroundTransparency = 1, LayoutOrder = order, Active = true }, convoList) round(header, 8) hover(header, C.hover, 0.5) local chev = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(2, 8), Size = UDim2.fromOffset(16, 16) }, header) local chevIcon = icon("chevron", chev, C.subtext, 14) chevIcon.Rotation = proj.expanded and 0 or -90 local folderIcon = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(22, 8), Size = UDim2.fromOffset(16, 16) }, header) icon("folder", folderIcon, C.subtext, 16) text({ Position = UDim2.fromOffset(42, 0), Size = UDim2.new(1, -96, 1, 0), Text = proj.name, Font = Enum.Font.GothamMedium, TextSize = 13, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, header) local addBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -6, 0.5, 0), Size = UDim2.fromOffset(22, 22), BackgroundTransparency = 1, Active = true }, header) icon("plus", addBtn, C.subtext, 14) if #proj.conversations > 0 then local pill = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -32, 0.5, 0), Size = UDim2.fromOffset(20, 18), BackgroundColor3 = C.hover, BorderSizePixel = 0, ZIndex = 2 }, header) round(pill, 9) text({ Size = UDim2.fromScale(1, 1), Text = tostring(#proj.conversations), TextSize = 11, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Center, TextYAlignment = Enum.TextYAlignment.Center, TextWrapped = false, ZIndex = 3 }, pill) end header.InputBegan:Connect(function(io) if io.UserInputType == Enum.UserInputType.MouseButton2 then showContextMenu(io.Position.X, io.Position.Y, { { text = "Rename Folder", callback = function() showRenamePrompt(proj.name, function(newName) proj.name = newName refreshSidebar() end) end }, { text = "Delete Folder", color = C.error, callback = function() local idx = table.find(projects, proj) if idx then table.remove(projects, idx) end if #projects == 0 then newProject("General Chats") end if proj == currentProject then selectProject(projects[1]) end refreshSidebar() end } }) end end) header.MouseButton1Click:Connect(function() proj.expanded = not proj.expanded refreshSidebar() end) addBtn.MouseButton1Click:Connect(function() selectProject(proj) newConversation(true) end) if proj.expanded then for _, convo in ipairs(proj.conversations) do if q == "" or convo.title:lower():find(q, 1, true) then order = order + 1 local selected = convo == current local item = button({ Size = UDim2.new(1, -16, 0, 30), Position = UDim2.fromOffset(16, 0), BackgroundColor3 = C.hover, BackgroundTransparency = selected and 0 or 1, LayoutOrder = order, Active = true }, convoList) round(item, 8) text({ Position = UDim2.fromOffset(12, 0), Size = UDim2.new(1, -36, 1, 0), Text = convo.title, TextSize = 13, TextColor3 = selected and C.text or C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, item) local del = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -6, 0.5, 0), Size = UDim2.fromOffset(24, 24), Visible = selected, BackgroundTransparency = 1, Active = true }, item) local dots = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(16, 16), Position = UDim2.fromOffset(4, 4) }, del) for i = 1, 3 do local dot = new("Frame", { BackgroundColor3 = C.subtext, Size = UDim2.fromOffset(2, 2), Position = UDim2.fromOffset(0, (i-2)*5), BorderSizePixel = 0 }, dots) round(dot, 1) end item.InputBegan:Connect(function(io) if io.UserInputType == Enum.UserInputType.MouseButton2 then showContextMenu(io.Position.X, io.Position.Y, { { text = "Rename Chat", callback = function() showRenamePrompt(convo.title, function(newName) convo.title = newName refreshSidebar() if convo == current then titleLabel.Text = convo.title end end) end }, { text = "Copy Text", callback = function() local allText = "" for _, m in ipairs(convo.messages) do if m.text then allText = allText .. m.text .. "\n\n" end end copyToClipboard(allText) end }, { text = "Delete Chat", color = C.error, callback = function() if convo == current and busy then cancelFlag = true end local idx = table.find(proj.conversations, convo) if idx then table.remove(proj.conversations, idx) end if convo == current then if #proj.conversations == 0 then local c = { title = "New chat", messages = {} } table.insert(proj.conversations, c) selectConversation(c) else selectConversation(proj.conversations[math.min(idx, #proj.conversations)]) end else refreshSidebar() end end } }) end end) item.MouseEnter:Connect(function() del.Visible = true if not selected then tween(item, 0.1, { BackgroundTransparency = 0.5 }) end end) item.MouseLeave:Connect(function() del.Visible = selected if not selected then tween(item, 0.1, { BackgroundTransparency = 1 }) end end) item.MouseButton1Click:Connect(function() selectProject(proj) selectConversation(convo) end) del.MouseButton1Click:Connect(function() if convo == current and busy then cancelFlag = true end local idx = table.find(proj.conversations, convo) if idx then table.remove(proj.conversations, idx) end if convo == current then if #proj.conversations == 0 then local c = { title = "New chat", messages = {} } table.insert(proj.conversations, c) selectConversation(c) else selectConversation(proj.conversations[math.min(idx, #proj.conversations)]) end else refreshSidebar() end end) end end end end end end local function updateThinkPill() local m = MODELS[modelIndex] thinkLabel.Text = m.think if thinkOn then thinkPill.BackgroundColor3 = m.accent thinkPill.BackgroundTransparency = 0.82 thinkStroke.Color = m.accent thinkLabel.TextColor3 = m.accent thinkDot.BackgroundColor3 = m.accent else thinkPill.BackgroundTransparency = 1 thinkStroke.Color = C.stroke thinkLabel.TextColor3 = C.subtext thinkDot.BackgroundColor3 = C.subtext end end local function applyTheme(name) local palette = THEMES[name] or THEMES["Claude"] settings.theme = THEMES[name] and name or "Claude" local oldPalette = ACTIVE_PALETTE for key, value in pairs(palette) do C[key] = value end ACTIVE_PALETTE = palette local function replaceColor(color) for key, oldColor in pairs(oldPalette) do if color == oldColor then return palette[key] or color end end return color end for _, obj in ipairs(gui:GetDescendants()) do if obj:IsA("GuiObject") then obj.BackgroundColor3 = replaceColor(obj.BackgroundColor3) if obj:IsA("TextLabel") or obj:IsA("TextButton") or obj:IsA("TextBox") then obj.TextColor3 = replaceColor(obj.TextColor3) if obj:IsA("TextBox") then obj.PlaceholderColor3 = replaceColor(obj.PlaceholderColor3) end end if obj:IsA("ImageLabel") or obj:IsA("ImageButton") then obj.ImageColor3 = replaceColor(obj.ImageColor3) end end if obj:IsA("UIStroke") then obj.Color = replaceColor(obj.Color) end end window.BackgroundColor3 = C.bg sidebar.BackgroundColor3 = C.sidebar composer.BackgroundColor3 = C.panel searchBox.BackgroundColor3 = C.bg messageArea.ScrollBarImageColor3 = C.stroke convoList.ScrollBarImageColor3 = C.stroke userCard.BackgroundColor3 = C.panel avatar.BackgroundColor3 = C.hover input.TextColor3 = C.text input.PlaceholderColor3 = C.subtext searchBox.TextColor3 = C.text searchBox.PlaceholderColor3 = C.subtext updateModelChip() updateThinkPill() updateHeaderSubtitle() pcall(function() if current then renderMessages() end end) pcall(function() refreshSidebar() end) pcall(function() layoutSidebar() end) end local function applyModel() local m = MODELS[modelIndex] if settings.customModel and settings.customModel.id ~= m.id then settings.customModel = nil settings.modelId = nil saveSettings() end if API.blockedModels then API.blockedModels = {} end updateModelChip() local logoModel = settings.autoLogo == false and MODELS[1] or m if brandSpark then brandSpark:Destroy() end brandSpark = brandLogo(sideInner, logoModel, 22) brandSpark.Position = UDim2.fromOffset(18, 17) if toggleSpark then toggleSpark:Destroy() end toggleSpark = brandLogo(toggleBtn, logoModel, 22) toggleSpark.Position = UDim2.fromOffset(12, 12) input.PlaceholderText = "Message " .. m.brandName .. "..." if sideProviderLabel then sideProviderLabel.Text = providerLabel(m) end sendBtn.BackgroundColor3 = m.accent recolor(plusIcon, m.accent) updateThinkPill() updateHeaderSubtitle() if workspaceRefresh then workspaceRefresh() end if current and #current.messages == 0 then renderMessages() end end thinkPill.MouseButton1Click:Connect(function() thinkOn = not thinkOn updateThinkPill() end) local menu, catcher local function closeMenu() if menu then local m = menu menu = nil tween(m, 0.12, { BackgroundTransparency = 1 }) for _, d in ipairs(m:GetDescendants()) do if d:IsA("GuiObject") then pcall(function() tween(d, 0.1, { BackgroundTransparency = 1, TextTransparency = 1 }) end) end end task.delay(0.14, function() if m.Parent then m:Destroy() end end) end if catcher then catcher:Destroy() catcher = nil end end function openMenu() if menu then closeMenu() return end catcher = button({ Size = UDim2.fromScale(1, 1), ZIndex = 50 }, window) catcher.MouseButton1Click:Connect(closeMenu) local w, itemH = 220, 40 local maxH = math.min((#MODELS + #API.CATALOGUE_ORDER) * itemH + 10, 360) menu = new("Frame", { Size = UDim2.fromOffset(w, maxH), BackgroundColor3 = C.panel, ZIndex = 51 }, window) round(menu, 12) stroke(menu) menu.ClipsDescendants = true local scroll = new("ScrollingFrame", { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 52, }, menu) local x = modelBtn.AbsolutePosition.X + modelBtn.AbsoluteSize.X - window.AbsolutePosition.X - w local y = modelBtn.AbsolutePosition.Y - window.AbsolutePosition.Y - maxH - 8 menu.Position = UDim2.fromOffset(math.max(8, x), math.max(8, y)) menu.BackgroundTransparency = 1 tween(menu, 0.14, { BackgroundTransparency = 0 }) for i, m in ipairs(MODELS) do local it = button({ Position = UDim2.fromOffset(4, 4 + (i - 1) * itemH), Size = UDim2.new(1, -8, 0, itemH - 4), BackgroundColor3 = C.hover, BackgroundTransparency = i == modelIndex and 0 or 1, ZIndex = 53 }, scroll) round(it, 8) hover(it, C.hover, 0) local lh = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(8, 8), Size = UDim2.fromOffset(18, 18), ZIndex = 54 }, it) brandLogo(lh, m, 18) text({ Position = UDim2.fromOffset(32, 2), Size = UDim2.new(1, -56, 0, 16), Text = m.short, Font = Enum.Font.GothamMedium, TextSize = 12, TextWrapped = false, ZIndex = 54 }, it) text({ Position = UDim2.fromOffset(32, 18), Size = UDim2.new(1, -56, 0, 14), Text = m.desc, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, ZIndex = 54 }, it) if i == modelIndex then local ck = new("Frame", { BackgroundTransparency = 1, AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -8, 0.5, 0), Size = UDim2.fromOffset(14, 14), ZIndex = 54 }, it) icon("check", ck, m.accent, 14) end it.MouseButton1Click:Connect(function() modelIndex = i closeMenu() applyModel() end) end for i, key in ipairs(API.CATALOGUE_ORDER) do local spec = API.CATALOGUES[key] local catRow = button({ Position = UDim2.fromOffset(4, 4 + (#MODELS + i - 1) * itemH), Size = UDim2.new(1, -8, 0, itemH - 4), BackgroundColor3 = C.hover, BackgroundTransparency = 1, ZIndex = 53 }, scroll) round(catRow, 8) hover(catRow, C.hover, 0) local catIcon = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(8, 8), Size = UDim2.fromOffset(18, 18), ZIndex = 54 }, catRow) icon("search", catIcon, C.subtext, 16) text({ Position = UDim2.fromOffset(32, 2), Size = UDim2.new(1, -56, 0, 16), Text = spec.title, Font = Enum.Font.GothamMedium, TextSize = 12, TextWrapped = false, ZIndex = 54 }, catRow) text({ Position = UDim2.fromOffset(32, 18), Size = UDim2.new(1, -56, 0, 14), Text = spec.blurb, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, ZIndex = 54 }, catRow) catRow.MouseButton1Click:Connect(function() closeMenu() openCataloguePicker(key) end) end end modelBtn.MouseButton1Click:Connect(openMenu) function placeholderFor(m) return "Message " .. m.brandName .. "..." end local function flashPlaceholder(msgText) input.PlaceholderText = msgText task.delay(3, function() input.PlaceholderText = placeholderFor(MODELS[modelIndex]) end) end function refreshAttachStrip() for _, ch in ipairs(attachRow:GetChildren()) do if ch:IsA("GuiObject") then ch:Destroy() end end for i, att in ipairs(attachments) do local th = new("ImageLabel", { Size = UDim2.fromOffset(100, 58), BackgroundColor3 = C.bg, ScaleType = Enum.ScaleType.Crop, LayoutOrder = i }, attachRow) round(th, 10) stroke(th) pcall(function() th.ImageContent = Content.fromObject(att.editable) end) local x = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -3, 0, 3), Size = UDim2.fromOffset(18, 18), BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 0.35 }, th) round(x, 9) icon("cross", x, Color3.new(1, 1, 1), 10) x.MouseButton1Click:Connect(function() table.remove(attachments, i) refreshAttachStrip() end) end updateComposer() end function attachScreenshot() if #attachments >= MAX_ATTACH then flashPlaceholder("Max " .. MAX_ATTACH .. " images per message") return end flashPlaceholder("Capturing screenshot...") local att, err = captureAttachment() if not att then flashPlaceholder("Screenshot failed: " .. tostring(err):sub(1, 70)) return end table.insert(attachments, att) input.PlaceholderText = placeholderFor(MODELS[modelIndex]) refreshAttachStrip() end local function makeSwitch(parent, on, accent) local function pos(v) return UDim2.new(0, v and 17 or 3, 0.5, 0) end local sw = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -12, 0.5, 0), Size = UDim2.fromOffset(34, 20), BackgroundColor3 = on and accent or C.stroke }, parent) round(sw, 10) local knob = new("Frame", { AnchorPoint = Vector2.new(0, 0.5), Position = pos(on), Size = UDim2.fromOffset(14, 14), BackgroundColor3 = Color3.new(1, 1, 1) }, sw) round(knob, 7) return function(v) tween(sw, 0.12, { BackgroundColor3 = v and accent or C.stroke }) tween(knob, 0.12, { Position = pos(v) }) end end function openToolsMenu() if menu then closeMenu() return end catcher = button({ Size = UDim2.fromScale(1, 1), ZIndex = 50 }, window) catcher.MouseButton1Click:Connect(closeMenu) local w, itemH = 300, 56 local rows = { { kind = "action", icon = "image", title = "Screenshot the game", desc = "Attach what you see" }, { kind = "toggle", key = "console", icon = "terminal", title = "Console access", desc = "AI may read errors when needed" }, { kind = "toggle", key = "screen", icon = "image", title = "Game vision", desc = "AI may look at the game itself" }, { kind = "toggle", key = "web", icon = "globe", title = "Web search", desc = "AI may look things up online" }, } local h = #rows * itemH + 12 menu = new("Frame", { Size = UDim2.fromOffset(w, h), BackgroundColor3 = C.panel, ZIndex = 51 }, window) round(menu, 14) stroke(menu) local x = plusBtn.AbsolutePosition.X - window.AbsolutePosition.X local y = plusBtn.AbsolutePosition.Y - window.AbsolutePosition.Y - h - 8 menu.Position = UDim2.fromOffset(math.max(8, math.min(x, window.AbsoluteSize.X - w - 8)), math.max(8, y)) local accent = MODELS[modelIndex].accent for i, rw in ipairs(rows) do local it = button({ Position = UDim2.fromOffset(6, 6 + (i - 1) * itemH), Size = UDim2.new(1, -12, 0, itemH - 4), BackgroundColor3 = C.hover, BackgroundTransparency = 1 }, menu) round(it, 10) hover(it, C.hover, 0) local ih = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(12, 14), Size = UDim2.fromOffset(22, 22) }, it) icon(rw.icon, ih, C.text, 20) text({ Position = UDim2.fromOffset(46, 7), Size = UDim2.new(1, -110, 0, 20), Text = rw.title, Font = Enum.Font.GothamMedium, TextSize = 14, TextWrapped = false }, it) text({ Position = UDim2.fromOffset(46, 27), Size = UDim2.new(1, -110, 0, 16), Text = rw.desc, TextSize = 12, TextColor3 = C.subtext, TextWrapped = false }, it) if rw.kind == "toggle" then local setSwitch = makeSwitch(it, settings[rw.key], accent) it.MouseButton1Click:Connect(function() settings[rw.key] = not settings[rw.key] setSwitch(settings[rw.key]) end) else it.MouseButton1Click:Connect(function() closeMenu() task.spawn(attachScreenshot) end) end end end plusBtn.MouseButton1Click:Connect(openToolsMenu) function openPermissionMenu() if menu then closeMenu() return end catcher = button({ Size = UDim2.fromScale(1, 1), ZIndex = 50 }, window) catcher.MouseButton1Click:Connect(closeMenu) local w, itemH = 300, 58 local h = #PERMISSION_MODES * itemH + 12 menu = new("Frame", { Size = UDim2.fromOffset(w, h), BackgroundColor3 = C.panel, ZIndex = 51 }, window) round(menu, 14) stroke(menu) local x = moreBtn.AbsolutePosition.X + moreBtn.AbsoluteSize.X - window.AbsolutePosition.X - w local y = moreBtn.AbsolutePosition.Y - window.AbsolutePosition.Y - h - 8 menu.Position = UDim2.fromOffset(math.max(8, math.min(x, window.AbsoluteSize.X - w - 8)), math.max(8, y)) local accent = MODELS[modelIndex].accent for i, mode in ipairs(PERMISSION_MODES) do local selected = settings.permission == mode.id local it = button({ Position = UDim2.fromOffset(6, 6 + (i - 1) * itemH), Size = UDim2.new(1, -12, 0, itemH - 4), BackgroundColor3 = C.hover, BackgroundTransparency = selected and 0 or 1, Text = "", TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 52 }, menu) round(it, 10) hover(it, C.hover, 0) local titleColor = mode.accent and accent or (selected and C.text or C.subtext) text({ Position = UDim2.fromOffset(14, 8), Size = UDim2.new(1, -44, 0, 20), Text = mode.title, Font = Enum.Font.GothamMedium, TextSize = 14, TextColor3 = titleColor, ZIndex = 53 }, it) text({ Position = UDim2.fromOffset(14, 28), Size = UDim2.new(1, -44, 0, 16), Text = mode.desc, TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 53 }, it) if selected then local ck = icon("check", it, accent, 16) ck.AnchorPoint = Vector2.new(1, 0.5) ck.Position = UDim2.new(1, -14, 0.5, 0) ck.ZIndex = 53 end it.MouseButton1Click:Connect(function() settings.permission = mode.id closeMenu() end) end end moreBtn.MouseButton1Click:Connect(openPermissionMenu) local settingsPanel function openSettings(initialTab) if settingsPanel then if API.settingsUI and API.settingsUI.close and API.settingsUI.isOpen and API.settingsUI.isOpen() then API.settingsUI.close() return end local old = settingsPanel settingsPanel = nil tween(old, 0.12, { BackgroundTransparency = 1 }) task.delay(0.14, function() if old.Parent then old:Destroy() end end) return end closeMenu() local accent = MODELS[modelIndex].accent local overlay = new("Frame", { Name = "SettingsOverlay", Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 1, ZIndex = 60 }, window) settingsPanel = overlay local catcher = button({ Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, ZIndex = 60 }, overlay) local panel = new("Frame", { Name = "SettingsPanel", AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.new(0.94, 0, 0.92, 0), BackgroundColor3 = C.panel, ZIndex = 61 }, overlay) round(panel, 16) stroke(panel) new("UISizeConstraint", { MinSize = Vector2.new(600, 420), MaxSize = Vector2.new(1000, 680) }, panel) local panelScale = new("UIScale", { Scale = 0.97 }, panel) tween(overlay, 0.16, { BackgroundTransparency = 0.45 }) tween(panelScale, 0.18, { Scale = 1 }) local closing = false local keyConn local function closePanel() if closing then return end closing = true if settingsPanel == overlay then settingsPanel = nil end if keyConn then pcall(function() keyConn:Disconnect() end) end tween(panelScale, 0.13, { Scale = 0.96 }) tween(overlay, 0.13, { BackgroundTransparency = 1 }) task.delay(0.15, function() if overlay.Parent then overlay:Destroy() end end) end catcher.MouseButton1Click:Connect(closePanel) text({ Position = UDim2.fromOffset(22, 13), Size = UDim2.new(1, -80, 0, 24), Text = "Settings", Font = Enum.Font.GothamBold, TextSize = 19, ZIndex = 62 }, panel) text({ Position = UDim2.fromOffset(22, 37), Size = UDim2.new(1, -80, 0, 16), Text = "Changes apply immediately · Save keeps them for next time", TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 62 }, panel) local close = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -12, 0, 12), Size = UDim2.fromOffset(30, 30), BackgroundColor3 = C.hover, ZIndex = 62 }, panel) round(close, 8) hover(close, C.hover, 0) icon("cross", close, C.subtext, 14) close.MouseButton1Click:Connect(closePanel) new("Frame", { Position = UDim2.fromOffset(0, 64), Size = UDim2.new(1, 0, 0, 1), BackgroundColor3 = C.stroke, BorderSizePixel = 0, ZIndex = 62 }, panel) local navCol = new("Frame", { Position = UDim2.fromOffset(12, 78), Size = UDim2.new(0, 200, 1, -146), BackgroundTransparency = 1, ZIndex = 62 }, panel) local navSearch = new("TextBox", { Size = UDim2.new(1, 0, 0, 32), BackgroundColor3 = C.bg, Text = "", PlaceholderText = "Search settings", PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = 12, Font = Enum.Font.Gotham, ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 63 }, navCol) round(navSearch, 8) stroke(navSearch, C.stroke) pad(navSearch, 0, 10, 0, 32) local searchIcon = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(11, 10), Size = UDim2.fromOffset(14, 14), ZIndex = 64 }, navSearch) icon("search", searchIcon, C.subtext, 14) local navScroll = new("ScrollingFrame", { Position = UDim2.fromOffset(0, 42), Size = UDim2.new(1, 0, 1, -42), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), ZIndex = 63 }, navCol) local content = new("ScrollingFrame", { Position = UDim2.fromOffset(228, 78), Size = UDim2.new(1, -246, 1, -146), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 62 }, panel) new("UIListLayout", { Padding = UDim.new(0, 16), SortOrder = Enum.SortOrder.LayoutOrder }, content) pad(content, 4, 10, 20, 0) local PAGES = { { id = "General", icon = "settings", group = "Assistant", desc = "Workspace, agent behaviour and startup" }, { id = "Personality", icon = "person", group = "Assistant", desc = "Extra instructions added to every reply" }, { id = "Providers", icon = "globe", group = "Models", desc = "Where requests go and which model answers" }, { id = "Appearance", icon = "image", group = "Workspace", desc = "Theme, motion and interface scale" }, { id = "Permissions", icon = "terminal", group = "Workspace", desc = "What the assistant may read, run and change" }, { id = "API keys", icon = "code", group = "Account", desc = "Provider credentials and where they live" }, } local PAGE_INFO = {} for _, page in ipairs(PAGES) do PAGE_INFO[page.id] = page end local SEARCH = { { page = "General", title = "Auto compact", desc = "Fold older turns into a summary" }, { page = "General", title = "Logo follows model", desc = "Show the selected model's mark" }, { page = "General", title = "Detected game", desc = "Context sent with every request" }, { page = "Personality", title = "Personality", desc = "Custom instructions for the assistant" }, { page = "Personality", title = "Starting points", desc = "Preset instruction chips" }, { page = "Providers", title = "Provider", desc = "OpenCode Zen or AgentRouter" }, { page = "Providers", title = "Active model", desc = "Chosen model for this session" }, { page = "Providers", title = "OpenCode key", desc = "Free models use the shared pool" }, { page = "Providers", title = "AgentRouter key", desc = "Required for Claude and GPT models" }, { page = "Appearance", title = "Theme", desc = "Palette used across chats and panels" }, { page = "Appearance", title = "Motion", desc = "Speed of panels, tabs and menus" }, { page = "Appearance", title = "Interface scale", desc = "Zoom the window contents" }, { page = "Appearance", title = "Transcript width", desc = "How wide assistant replies can grow" }, { page = "Permissions", title = "Console access", desc = "Read output the game logs" }, { page = "Permissions", title = "Game vision", desc = "Capture the screen for visual context" }, { page = "Permissions", title = "Web search", desc = "Let the model look things up online" }, { page = "Permissions", title = "Read only", desc = "Inspection only, nothing changes" }, { page = "Permissions", title = "Ask first", desc = "Writes wait for confirmation" }, { page = "Permissions", title = "Auto", desc = "Writes run, execution still asks" }, { page = "Permissions", title = "Allow everything", desc = "No prompts at all" }, { page = "API keys", title = "Settings file", desc = "Where keys and preferences are stored" }, } local navButtons, builders = {}, {} local rowOrder, cardCount = 0, {} local openPage, currentPage = nil, "General" local suppressSearch = false local function indexOf(list, value) for i, item in ipairs(list) do if item == value then return i end end return 1 end local function clearContent() for _, child in ipairs(content:GetChildren()) do if child:IsA("GuiObject") then child:Destroy() end end rowOrder, cardCount = 0, {} end local function pageHead(title, blurb) rowOrder = rowOrder + 1 local head = new("Frame", { Size = UDim2.new(1, 0, 0, 44), BackgroundTransparency = 1, LayoutOrder = rowOrder }, content) text({ Position = UDim2.fromOffset(0, 2), Size = UDim2.new(1, 0, 0, 20), Text = title, Font = Enum.Font.GothamBold, TextSize = 17, ZIndex = 64 }, head) text({ Position = UDim2.fromOffset(0, 24), Size = UDim2.new(1, 0, 0, 16), Text = blurb or "", TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, head) end local function section(label) rowOrder = rowOrder + 1 local wrap = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = rowOrder }, content) new("UIListLayout", { Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, wrap) text({ Size = UDim2.new(1, 0, 0, 14), Text = label:upper(), Font = Enum.Font.GothamBold, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 1, ZIndex = 64 }, wrap) local card = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundColor3 = C.bg, LayoutOrder = 2 }, wrap) round(card, 10) stroke(card, C.stroke) new("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder }, card) cardCount[card] = 0 return card end local function row(card, title, desc, height) cardCount[card] = (cardCount[card] or 0) + 1 local index = cardCount[card] local r = new("Frame", { Size = UDim2.new(1, 0, 0, height or 58), BackgroundTransparency = 1, LayoutOrder = index }, card) pad(r, 0, 14, 0, 14) if index > 1 then new("Frame", { Position = UDim2.new(0, 14, 0, 0), Size = UDim2.new(1, -28, 0, 1), BackgroundColor3 = C.stroke, BackgroundTransparency = 0.5, BorderSizePixel = 0, ZIndex = 63 }, r) end text({ Position = UDim2.fromOffset(0, 9), Size = UDim2.new(1, -216, 0, 18), Text = title, TextSize = 13, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, r) if desc and desc ~= "" then text({ Position = UDim2.fromOffset(0, 27), Size = UDim2.new(1, -216, 0, 16), Text = desc, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, r) end return r end local function pill(parent, value, color, y) local box = new("Frame", { AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, y or 10), Size = UDim2.fromOffset(0, 22), AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.panel, ZIndex = 64 }, parent) round(box, 11) stroke(box, C.stroke) pad(box, 0, 10, 0, 10) text({ Size = UDim2.fromOffset(0, 22), AutomaticSize = Enum.AutomaticSize.X, Text = value, TextSize = 10, TextColor3 = color or C.subtext, TextWrapped = false, ZIndex = 65 }, box) return box end local function segmented(parent, options, selectedIndex, onPick) local box = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(4, 30), AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.panel, ZIndex = 64 }, parent) round(box, 9) stroke(box, C.stroke) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 2), VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, box) pad(box, 2, 2, 2, 2) local chips = {} local function select(i, silent) for idx, chip in ipairs(chips) do tween(chip, 0.14, { BackgroundTransparency = idx == i and 0 or 1, TextColor3 = idx == i and C.text or C.subtext }) end if not silent and onPick then onPick(i) end end for i, label in ipairs(options) do local chip = button({ Size = UDim2.fromOffset(0, 26), AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.hover, BackgroundTransparency = i == selectedIndex and 0 or 1, Text = label, TextSize = 12, TextWrapped = false, TextColor3 = i == selectedIndex and C.text or C.subtext, LayoutOrder = i, ZIndex = 65 }, box) round(chip, 7) pad(chip, 0, 12, 0, 12) chips[i] = chip chip.MouseButton1Click:Connect(function() select(i) end) end return select end local function toggle(parent, value, onChange) local on = value and true or false local track = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(40, 22), BackgroundColor3 = on and accent or C.hover, ZIndex = 64 }, parent) round(track, 11) local knob = new("Frame", { AnchorPoint = Vector2.new(0, 0.5), Position = UDim2.new(0, on and 19 or 3, 0.5, 0), Size = UDim2.fromOffset(16, 16), BackgroundColor3 = Color3.new(1, 1, 1), BorderSizePixel = 0, ZIndex = 65 }, track) round(knob, 8) track.MouseButton1Click:Connect(function() on = not on tween(track, 0.14, { BackgroundColor3 = on and accent or C.hover }) tween(knob, 0.16, { Position = UDim2.new(0, on and 19 or 3, 0.5, 0) }) if onChange then onChange(on) end end) return track end local function optionRow(card, title, desc, selected, onPick) cardCount[card] = (cardCount[card] or 0) + 1 local index = cardCount[card] local r = button({ Size = UDim2.new(1, 0, 0, 64), BackgroundTransparency = 1, LayoutOrder = index, ZIndex = 63 }, card) pad(r, 0, 14, 0, 14) if index > 1 then new("Frame", { Position = UDim2.new(0, 14, 0, 0), Size = UDim2.new(1, -28, 0, 1), BackgroundColor3 = C.stroke, BackgroundTransparency = 0.5, BorderSizePixel = 0 }, r) end text({ Position = UDim2.fromOffset(0, 12), Size = UDim2.new(1, -58, 0, 18), Text = title, TextSize = 13, ZIndex = 64 }, r) text({ Position = UDim2.fromOffset(0, 31), Size = UDim2.new(1, -58, 0, 16), Text = desc, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, r) local dot = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(20, 20), BackgroundColor3 = selected and accent or C.panel, BackgroundTransparency = selected and 0 or 1, BorderSizePixel = 0, ZIndex = 64 }, r) round(dot, 10) if selected then icon("check", dot, Color3.new(1, 1, 1), 12) else stroke(dot, C.stroke, 1.5) end r.MouseButton1Click:Connect(function() onPick() end) end local function themeGrid(card) cardCount[card] = (cardCount[card] or 0) + 1 local holder = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1, LayoutOrder = cardCount[card] }, card) pad(holder, 12, 14, 10, 14) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Wraps = true, Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder }, holder) local borders = {} local function paint(name) for key, border in pairs(borders) do border.Color = key == name and accent or C.stroke border.Thickness = key == name and 2 or 1 end end for i, name in ipairs({ "Claude", "ChatGPT", "DeepSeek", "GLM", "Kimi" }) do local pal = THEMES[name] local b = button({ Size = UDim2.fromOffset(94, 68), BackgroundColor3 = pal.bg, LayoutOrder = i, ZIndex = 64 }, holder) round(b, 8) borders[name] = stroke(b, C.stroke) local lineA = new("Frame", { Position = UDim2.fromOffset(10, 12), Size = UDim2.fromOffset(42, 7), BackgroundColor3 = pal.text, BorderSizePixel = 0, ZIndex = 65 }, b) round(lineA, 3) local lineB = new("Frame", { Position = UDim2.fromOffset(10, 24), Size = UDim2.fromOffset(60, 5), BackgroundColor3 = pal.subtext, BorderSizePixel = 0, ZIndex = 65 }, b) round(lineB, 2) local swatch = new("Frame", { Position = UDim2.fromOffset(10, 38), Size = UDim2.fromOffset(30, 16), BackgroundColor3 = pal.panel, BorderSizePixel = 0, ZIndex = 65 }, b) round(swatch, 4) text({ Position = UDim2.fromOffset(10, 51), Size = UDim2.new(1, -16, 0, 12), Text = name, TextSize = 9, TextColor3 = pal.subtext, TextWrapped = false, ZIndex = 65 }, b) b.MouseButton1Click:Connect(function() applyTheme(name) settings.theme = name paint(name) end) end paint(settings.theme) end local function openModelPicker() local layer = new("Frame", { Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 0.45, ZIndex = 80 }, panel) local backdrop = button({ Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, ZIndex = 80 }, layer) local box = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(360, 380), BackgroundColor3 = C.panel, ZIndex = 81 }, layer) round(box, 14) stroke(box) text({ Position = UDim2.fromOffset(16, 12), Size = UDim2.new(1, -32, 0, 20), Text = "Choose a model", Font = Enum.Font.GothamBold, TextSize = 15, ZIndex = 82 }, box) local list = new("ScrollingFrame", { Position = UDim2.fromOffset(8, 40), Size = UDim2.new(1, -16, 1, -48), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 82 }, box) new("UIListLayout", { Padding = UDim.new(0, 2) }, list) for i, m in ipairs(MODELS) do local item = button({ Size = UDim2.new(1, 0, 0, 46), BackgroundColor3 = C.hover, BackgroundTransparency = i == modelIndex and 0 or 1, LayoutOrder = i, ZIndex = 82 }, list) round(item, 8) hover(item, C.hover, 0.1) local dot = new("Frame", { Position = UDim2.fromOffset(10, 14), Size = UDim2.fromOffset(18, 18), BackgroundColor3 = m.accent, BorderSizePixel = 0, ZIndex = 83 }, item) round(dot, 9) text({ Position = UDim2.fromOffset(36, 6), Size = UDim2.new(1, -60, 0, 16), Text = m.brandName .. " " .. m.short, TextSize = 12, ZIndex = 83 }, item) text({ Position = UDim2.fromOffset(36, 23), Size = UDim2.new(1, -60, 0, 14), Text = m.desc, TextSize = 10, TextColor3 = C.subtext, ZIndex = 83 }, item) if i == modelIndex then local mark = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(14, 14), BackgroundTransparency = 1, ZIndex = 83 }, item) icon("check", mark, m.accent, 14) end item.MouseButton1Click:Connect(function() modelIndex = i applyModel() layer:Destroy() openPage("Providers") end) end local orItem = button({ Size = UDim2.new(1, 0, 0, 46), BackgroundColor3 = C.hover, BackgroundTransparency = 1, LayoutOrder = #MODELS + 1, ZIndex = 82 }, list) round(orItem, 8) hover(orItem, C.hover, 0.1) local orDot = new("Frame", { Position = UDim2.fromOffset(10, 14), Size = UDim2.fromOffset(18, 18), BackgroundTransparency = 1, ZIndex = 83 }, orItem) icon("search", orDot, C.subtext, 16) text({ Position = UDim2.fromOffset(36, 6), Size = UDim2.new(1, -60, 0, 16), Text = "OpenRouter models", TextSize = 12, ZIndex = 83 }, orItem) text({ Position = UDim2.fromOffset(36, 23), Size = UDim2.new(1, -60, 0, 14), Text = "Search the live catalogue", TextSize = 10, TextColor3 = C.subtext, ZIndex = 83 }, orItem) orItem.MouseButton1Click:Connect(function() layer:Destroy() openPage("Providers") openOpenRouterPicker() end) backdrop.MouseButton1Click:Connect(function() layer:Destroy() end) end local function keyField(card, title, desc, key) local r = row(card, title, desc, 98) local state = pill(r, (settings[key] or "") ~= "" and "Set" or "Empty", (settings[key] or "") ~= "" and Color3.fromRGB(106, 200, 145) or C.subtext, 12) local stateLabel = state:FindFirstChildOfClass("TextLabel") local box = new("TextBox", { Position = UDim2.fromOffset(0, 47), Size = UDim2.new(1, -196, 0, 32), BackgroundColor3 = C.panel, Text = settings[key] or "", PlaceholderText = "Paste " .. title:lower(), PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = 12, Font = Enum.Font.Code, ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 64 }, r) round(box, 8) stroke(box, C.stroke) pad(box, 0, 10, 0, 10) box:GetPropertyChangedSignal("Text"):Connect(function() settings[key] = box.Text:match("^%s*(.-)%s*$") or "" if stateLabel and stateLabel.Parent then local filled = settings[key] ~= "" stateLabel.Text = filled and "Set" or "Empty" stateLabel.TextColor3 = filled and Color3.fromRGB(106, 200, 145) or C.subtext end end) local test = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 47), Size = UDim2.fromOffset(88, 32), BackgroundColor3 = C.hover, Text = "Test", TextSize = 12, ZIndex = 64 }, r) round(test, 8) stroke(test, C.stroke) hover(test, C.panel, 0) local clear = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -96, 0, 47), Size = UDim2.fromOffset(84, 32), BackgroundColor3 = C.hover, Text = "Clear", TextSize = 12, ZIndex = 64 }, r) round(clear, 8) stroke(clear, C.stroke) hover(clear, C.panel, 0) clear.MouseButton1Click:Connect(function() box.Text = "" settings[key] = "" if stateLabel and stateLabel.Parent then stateLabel.Text = "Empty" stateLabel.TextColor3 = C.subtext end end) test.MouseButton1Click:Connect(function() if not testApiConnection then return end if stateLabel and stateLabel.Parent then stateLabel.Text = "Testing…" stateLabel.TextColor3 = C.subtext end test.Text = "Testing…" test.Active = false local provider = key == "opencodeApiKey" and "opencode" or (key == "openrouterKey" and "openrouter" or "agentrouter") local previous = settings.provider settings.provider = provider testApiConnection(settings[key], function(ok, message) if stateLabel and stateLabel.Parent then local short = tostring(message or (ok and "Works" or "Failed")) if #short > 34 then short = short:sub(1, 31) .. "..." end stateLabel.Text = short stateLabel.TextColor3 = ok and Color3.fromRGB(106, 200, 145) or C.error end if test.Parent then test.Text = "Test" test.Active = true end settings.provider = previous end, provider) end) end builders.General = function() pageHead("General", PAGE_INFO.General.desc) local session = section("Session") local gameRow = row(session, "Detected game", "Sent as context with every request") pill(gameRow, gameName, C.subtext, 19) toggle(row(session, "Auto compact", "Fold older turns into a summary when a session gets long"), settings.autoCompact ~= false, function(v) settings.autoCompact = v end) local startup = section("Startup") toggle(row(startup, "Logo follows model", "Show the selected model's mark in the sidebar"), settings.autoLogo ~= false, function(v) settings.autoLogo = v applyModel() end) end builders.Personality = function() pageHead("Personality", PAGE_INFO.Personality.desc) local card = section("Custom instructions") local r = row(card, "Personality", "Added to the system message of every conversation", 176) local box = new("TextBox", { Position = UDim2.fromOffset(0, 47), Size = UDim2.new(1, -110, 0, 92), BackgroundColor3 = C.panel, Text = settings.personality or "", PlaceholderText = "Example: Be concise, explain the fix before the code.", PlaceholderColor3 = C.subtext, TextColor3 = C.text, Font = Enum.Font.Gotham, TextSize = 12, TextWrapped = true, TextYAlignment = Enum.TextYAlignment.Top, TextXAlignment = Enum.TextXAlignment.Left, ClearTextOnFocus = false, MultiLine = true, ZIndex = 64 }, r) round(box, 8) stroke(box, C.stroke) pad(box, 8, 10, 8, 10) local counter = text({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 47), Size = UDim2.fromOffset(96, 16), Text = tostring(#(settings.personality or "")) .. " chars", TextSize = 10, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false, ZIndex = 64 }, r) local clearBtn = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 70), Size = UDim2.fromOffset(96, 30), BackgroundColor3 = C.hover, Text = "Clear", TextSize = 11, ZIndex = 64 }, r) round(clearBtn, 8) hover(clearBtn, C.panel, 0) clearBtn.MouseButton1Click:Connect(function() box.Text = "" settings.personality = "" counter.Text = "0 chars" end) box:GetPropertyChangedSignal("Text"):Connect(function() settings.personality = box.Text if counter.Parent then counter.Text = tostring(#box.Text) .. " chars" end end) local presets = section("Starting points") local holder = new("Frame", { Size = UDim2.new(1, 0, 0, 52), BackgroundTransparency = 1, LayoutOrder = cardCount[presets] + 1 }, presets) pad(holder, 12, 14, 12, 14) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6), VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, holder) for i, preset in ipairs({ { "Concise", "Be concise. Lead with the answer, then the details." }, { "Explain code", "Explain code changes before showing them." }, { "Ask first", "Ask before guessing when the request is ambiguous." }, { "Roblox focus", "Prefer idiomatic Roblox Luau and mention performance costs." }, }) do local chip = button({ Size = UDim2.fromOffset(0, 28), AutomaticSize = Enum.AutomaticSize.X, BackgroundColor3 = C.hover, Text = preset[1], TextSize = 11, LayoutOrder = i, ZIndex = 64 }, holder) round(chip, 7) pad(chip, 0, 12, 0, 12) hover(chip, C.panel, 0) chip.MouseButton1Click:Connect(function() local current = settings.personality or "" local addition = preset[2] if current ~= "" then addition = current .. " " .. addition end settings.personality = addition box.Text = addition end) end end builders.Providers = function() pageHead("Providers", PAGE_INFO.Providers.desc) local routing = section("Routing") local routingNames = { "OpenCode", "AgentRouter", "OpenRouter" } local routingIds = { "opencode", "agentrouter", "openrouter" } segmented(row(routing, "Provider", "Where model requests are sent"), routingNames, math.max(1, indexOf(routingIds, settings.provider)), function(i) settings.provider = routingIds[i] if sideProviderLabel then sideProviderLabel.Text = providerLabel(MODELS[modelIndex]) end end) local modelRow = row(routing, "Active model", "Chosen model for this session") local picker = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(196, 34), BackgroundColor3 = C.panel, ZIndex = 64 }, modelRow) round(picker, 8) stroke(picker, C.stroke) hover(picker, C.hover, 0) local dot = new("Frame", { Position = UDim2.fromOffset(10, 9), Size = UDim2.fromOffset(16, 16), BackgroundColor3 = MODELS[modelIndex].accent, BorderSizePixel = 0, ZIndex = 65 }, picker) round(dot, 8) text({ Position = UDim2.fromOffset(34, 7), Size = UDim2.new(1, -44, 0, 20), Text = MODELS[modelIndex].brandName .. " " .. MODELS[modelIndex].short, TextSize = 12, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 65 }, picker) picker.MouseButton1Click:Connect(openModelPicker) local keys = section("Credentials") keyField(keys, "OpenCode key", "Optional. Free models use the shared free pool.", "opencodeApiKey") keyField(keys, "AgentRouter key", "Required for Claude and GPT models.", "apiKey") keyField(keys, "OpenRouter key", "Unlocks the full OpenRouter catalogue.", "openrouterKey") for _, prov in ipairs(API.CATALOGUE_ORDER) do local spec = API.CATALOGUES[prov] local browseRow = row(keys, spec.title, "Browse this provider's live catalogue") local browseBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(110, 30), BackgroundColor3 = C.hover, Text = "Browse", TextSize = 11, ZIndex = 64 }, browseRow) round(browseBtn, 8) stroke(browseBtn, C.stroke) hover(browseBtn, C.panel, 0) browseBtn.MouseButton1Click:Connect(function() openCataloguePicker(prov) end) end local status = section("Status") local statusRow = row(status, "Routing preference", "Falls back automatically when a provider refuses") local routingFirst = { opencode = "Zen first", agentrouter = "AgentRouter first", openrouter = "OpenRouter first" } pill(statusRow, routingFirst[settings.provider] or "Zen first", C.subtext, 18) end builders.Appearance = function() pageHead("Appearance", PAGE_INFO.Appearance.desc) themeGrid(section("Theme")) local motion = section("Motion") segmented(row(motion, "Transitions", "How quickly panels, tabs and menus move"), { "Full", "Subtle", "Off" }, indexOf({ "Full", "Subtle", "Off" }, settings.motion), function(i) settings.motion = ({ "Full", "Subtle", "Off" })[i] MOTION = settings.motion == "Off" and 0 or (settings.motion == "Subtle" and 0.6 or 1) end) local layoutSection = section("Layout") segmented(row(layoutSection, "Interface scale", "Zoom the window contents"), { "85%", "100%", "110%" }, indexOf({ 0.85, 1, 1.1 }, settings.uiScale), function(i) settings.uiScale = ({ 0.85, 1, 1.1 })[i] winScale.Scale = settings.uiScale end) segmented(row(layoutSection, "Transcript width", "How wide assistant replies can grow"), { "Narrow", "Wide" }, (settings.transcriptWidth or 740) >= 860 and 2 or 1, function(i) settings.transcriptWidth = i == 2 and 880 or 740 local constraint = column:FindFirstChildOfClass("UISizeConstraint") if constraint then constraint.MaxSize = Vector2.new(settings.transcriptWidth, math.huge) end end) end builders.Permissions = function() pageHead("Permissions", PAGE_INFO.Permissions.desc) local access = section("Data access") toggle(row(access, "Console access", "Read output the game logs"), settings.console ~= false, function(v) settings.console = v end) toggle(row(access, "Game vision", "Capture the screen for visual context"), settings.screen ~= false, function(v) settings.screen = v end) toggle(row(access, "Web search", "Let the model look things up online"), settings.web ~= false, function(v) settings.web = v end) local modes = section("Tool permissions") for _, mode in ipairs(PERMISSION_MODES) do optionRow(modes, mode.title, mode.desc, settings.permission == mode.id, function() settings.permission = mode.id openPage("Permissions") end) end local note = section("How prompts work") row(note, "Approvals time out", "A prompt left unanswered for 30 seconds counts as declined.") end builders["API keys"] = function() pageHead("API keys", PAGE_INFO["API keys"].desc) local keys = section("Credentials") keyField(keys, "AgentRouter key", "Required for Claude and GPT models.", "apiKey") keyField(keys, "OpenCode key", "Optional. Free models use the shared free pool.", "opencodeApiKey") keyField(keys, "OpenRouter key", "Unlocks the full OpenRouter catalogue.", "openrouterKey") local storage = section("Storage") local fileRow = row(storage, "Settings file", "Keys and preferences are written here only") pill(fileRow, "AgentRouterRobloxAI.json", C.subtext, 18) row(storage, "Plain text", "Treat this file like a password. Nothing is uploaded.") local danger = section("Reset") local clearRow = row(danger, "Remove all keys", "Clears the fields above and saves an empty key") local clearBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(120, 30), BackgroundColor3 = C.hover, Text = "Remove keys", TextSize = 11, ZIndex = 64 }, clearRow) round(clearBtn, 8) stroke(clearBtn, C.stroke) hover(clearBtn, C.error, 0.75) local armed = false clearBtn.MouseButton1Click:Connect(function() if not armed then armed = true clearBtn.Text = "Click to confirm" task.delay(3, function() if clearBtn.Parent then armed = false clearBtn.Text = "Remove keys" end end) return end settings.apiKey = "" settings.opencodeApiKey = "" settings.openrouterKey = "" AGENTROUTER_KEY, OPENCODE_KEY = "", "" saveSettings() openPage("API keys") end) end local function showResults(query) clearContent() local matches = {} for _, entry in ipairs(SEARCH) do if entry.title:lower():find(query, 1, true) or entry.desc:lower():find(query, 1, true) then matches[#matches + 1] = entry end end pageHead("Search", tostring(#matches) .. " setting" .. (#matches == 1 and "" or "s") .. " found") local card = section("Results") if #matches == 0 then row(card, "Nothing matches", "Try a shorter word, like theme, key or motion.") return end for _, entry in ipairs(matches) do cardCount[card] = (cardCount[card] or 0) + 1 local index = cardCount[card] local r = button({ Size = UDim2.new(1, 0, 0, 52), BackgroundTransparency = 1, LayoutOrder = index, ZIndex = 63 }, card) pad(r, 0, 14, 0, 14) if index > 1 then new("Frame", { Position = UDim2.new(0, 14, 0, 0), Size = UDim2.new(1, -28, 0, 1), BackgroundColor3 = C.stroke, BackgroundTransparency = 0.5, BorderSizePixel = 0 }, r) end text({ Position = UDim2.fromOffset(0, 9), Size = UDim2.new(1, -110, 0, 18), Text = entry.title, TextSize = 13, ZIndex = 64 }, r) text({ Position = UDim2.fromOffset(0, 27), Size = UDim2.new(1, -110, 0, 16), Text = entry.desc, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, r) pill(r, entry.page, C.subtext, 15) r.MouseButton1Click:Connect(function() suppressSearch = true navSearch.Text = "" suppressSearch = false openPage(entry.page) end) end end openPage = function(name) if not builders[name] then name = "General" end currentPage = name for pageName, entry in pairs(navButtons) do local active = pageName == name tween(entry.btn, 0.14, { BackgroundTransparency = active and 0.35 or 1 }) entry.label.TextColor3 = active and C.text or C.subtext entry.bar.Visible = active end clearContent() builders[name]() content.CanvasPosition = Vector2.new(0, 0) content.Position = UDim2.fromOffset(234, 78) tween(content, 0.16, { Position = UDim2.fromOffset(228, 78) }) end local navY, lastGroup = 2, nil for _, page in ipairs(PAGES) do if page.group ~= lastGroup then lastGroup = page.group text({ Position = UDim2.fromOffset(10, navY + 6), Size = UDim2.new(1, -20, 0, 12), Text = page.group:upper(), Font = Enum.Font.GothamBold, TextSize = 9, TextColor3 = C.subtext, TextWrapped = false, ZIndex = 64 }, navScroll) navY = navY + 24 end local b = button({ Position = UDim2.fromOffset(0, navY), Size = UDim2.new(1, 0, 0, 32), BackgroundColor3 = C.hover, BackgroundTransparency = 1, ZIndex = 63 }, navScroll) round(b, 8) hover(b, C.hover, 0.35) local bar = new("Frame", { Position = UDim2.fromOffset(0, 9), Size = UDim2.fromOffset(2, 14), BackgroundColor3 = accent, BorderSizePixel = 0, Visible = false, ZIndex = 65 }, b) round(bar, 1) local ic = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(12, 8), Size = UDim2.fromOffset(16, 16), ZIndex = 64 }, b) icon(page.icon, ic, C.subtext, 16) local label = text({ Position = UDim2.fromOffset(38, 0), Size = UDim2.new(1, -48, 1, 0), Text = page.id, TextSize = 12, TextColor3 = C.subtext, TextWrapped = false, ZIndex = 64 }, b) navButtons[page.id] = { btn = b, label = label, bar = bar } b.MouseButton1Click:Connect(function() suppressSearch = true navSearch.Text = "" suppressSearch = false openPage(page.id) end) navY = navY + 35 end navScroll.CanvasSize = UDim2.new(0, 0, 0, navY + 6) local function applySearch(raw) local query = tostring(raw or ""):lower():match("^%s*(.-)%s*$") for pageName, entry in pairs(navButtons) do local visible = query == "" if not visible then visible = pageName:lower():find(query, 1, true) ~= nil end if not visible then for _, item in ipairs(SEARCH) do if item.page == pageName and (item.title:lower():find(query, 1, true) or item.desc:lower():find(query, 1, true)) then visible = true break end end end entry.btn.Visible = visible end if query == "" then openPage(currentPage) else showResults(query) end end navSearch:GetPropertyChangedSignal("Text"):Connect(function() if suppressSearch then return end applySearch(navSearch.Text) end) text({ Position = UDim2.new(0, 20, 1, -60), Size = UDim2.new(0, 216, 0, 60), Text = "Stored locally · AgentRouterRobloxAI.json", TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, ZIndex = 64 }, panel) local saveNote = text({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -360, 1, -30), Size = UDim2.fromOffset(100, 16), Text = "", TextSize = 10, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false, ZIndex = 64 }, panel) local resetBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -256, 1, -30), Size = UDim2.fromOffset(96, 34), BackgroundColor3 = C.hover, Text = "Reset all", TextSize = 12, ZIndex = 64 }, panel) round(resetBtn, 9) stroke(resetBtn, C.stroke) hover(resetBtn, C.panel, 0) local saveBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -20, 1, -30), Size = UDim2.fromOffset(132, 34), BackgroundColor3 = accent, Text = "Save changes", TextSize = 13, ZIndex = 64 }, panel) round(saveBtn, 9) hover(saveBtn, accent, 0.12) local function applySettings() AGENTROUTER_KEY = settings.apiKey or "" OPENCODE_KEY = settings.opencodeApiKey or "" winScale.Scale = settings.uiScale or 1 local constraint = column:FindFirstChildOfClass("UISizeConstraint") if constraint then constraint.MaxSize = Vector2.new(settings.transcriptWidth or 740, math.huge) end MOTION = settings.motion == "Off" and 0 or (settings.motion == "Subtle" and 0.6 or 1) applyTheme(settings.theme) return saveSettings() end local function saveNow() local saved = applySettings() if API.settingsUI then API.settingsUI.lastSave = saved and "Saved" or "Applied" end saveNote.Text = saved and "Saved" or "Applied" saveNote.TextColor3 = saved and Color3.fromRGB(106, 200, 145) or C.subtext task.delay(2, function() if saveNote.Parent then saveNote.Text = "" end end) end local function resetNow() settings.theme = "Claude" settings.motion = "Full" settings.uiScale = 1 settings.transcriptWidth = 740 settings.autoLogo = true settings.autoCompact = true settings.permission = "ask" settings.console = true settings.screen = true settings.web = true settings.personality = "" winScale.Scale = 1 MOTION = 1 local constraint = column:FindFirstChildOfClass("UISizeConstraint") if constraint then constraint.MaxSize = Vector2.new(740, math.huge) end applyTheme("Claude") saveSettings() openPage(currentPage) saveNote.Text = "Defaults restored" saveNote.TextColor3 = Color3.fromRGB(106, 200, 145) task.delay(2, function() if saveNote.Parent then saveNote.Text = "" end end) end local resetArmed = false resetBtn.MouseButton1Click:Connect(function() if not resetArmed then resetArmed = true resetBtn.Text = "Confirm reset" task.delay(3, function() if resetBtn.Parent then resetArmed = false resetBtn.Text = "Reset all" end end) return end resetArmed = false resetBtn.Text = "Reset all" resetNow() end) saveBtn.MouseButton1Click:Connect(saveNow) API.settingsUI = { openPage = function(name) openPage(name) end, search = function(value) suppressSearch = true navSearch.Text = tostring(value or "") suppressSearch = false applySearch(value) end, save = saveNow, reset = resetNow, close = closePanel, panel = panel, isOpen = function() return settingsPanel == overlay end } pcall(function() keyConn = UserInputService.InputBegan:Connect(function(input, processed) if processed then return end if input.KeyCode == Enum.KeyCode.Escape and settingsPanel == overlay then closePanel() end end) end) openPage(initialTab or "General") end -- Measure the wrapped text ourselves. TextBox.TextBounds is unreliable while the box is -- being resized, which made long messages scroll sideways instead of growing downwards. function measureInputHeight() -- AbsoluteSize includes UIScale; convert back to the unscaled units used by Size. local scale = (winScale and winScale.Scale) or 1 if scale <= 0 then scale = 1 end local width = input.AbsoluteSize.X / scale if width < 80 then width = math.max(120, composer.AbsoluteSize.X / scale - 32) end local probe = input.Text if probe == "" then probe = "Ag" end local ok, bounds = pcall(function() return game:GetService("TextService"):GetTextSize(probe, 15, Enum.Font.Gotham, Vector2.new(width, 100000)) end) local textH = (ok and bounds and bounds.Y) or 15 local winH = (window.AbsoluteSize.Y > 0 and window.AbsoluteSize.Y or 600) / scale local maxH = math.clamp(winH * 0.42, 96, 220) return math.clamp(math.ceil(textH) + 4, 24, maxH) end function updateComposer() local h = measureInputHeight() local ah = #attachments > 0 and 70 or 0 attachRow.Visible = ah > 0 input.Position = UDim2.fromOffset(16, 14 + ah) input.Size = UDim2.new(1, -32, 0, h) local ch = h + 68 + ah composer.Size = UDim2.new(1, -32, 0, ch) messageArea.Size = UDim2.new(1, 0, 1, -(52 + ch + 24)) local canSend = busy or input.Text:match("%S") ~= nil or #attachments > 0 tween(sendBtn, 0.12, { BackgroundTransparency = canSend and 0 or 0.65 }) arrowIcon.Visible = not busy stopIcon.Visible = busy end input:GetPropertyChangedSignal("Text"):Connect(function() if input.Text:sub(-1) == "\n" and not UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then input.Text = input.Text:sub(1, -2) if not busy then send() end return end updateComposer() end) searchBox:GetPropertyChangedSignal("Text"):Connect(refreshSidebar) -- AgentRouter fingerprints the client. Codex models accept the Codex CLI signature, -- but Claude models are only accepted from a Claude Code client. API.CODEX_SPOOF = { ["Originator"] = "codex_cli_rs", ["User-Agent"] = "codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464", ["Version"] = "0.101.0", } API.CLAUDE_SPOOF = { ["User-Agent"] = "claude-cli/2.1.119 (external, cli)", ["x-app"] = "cli", ["X-Stainless-Arch"] = "x64", ["X-Stainless-Lang"] = "js", ["X-Stainless-OS"] = "Linux", ["X-Stainless-Package-Version"] = "0.60.0", ["X-Stainless-Runtime"] = "node", ["X-Stainless-Runtime-Version"] = "v22.14.0", ["anthropic-beta"] = "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,claude-code-20250219", ["anthropic-dangerous-direct-browser-access"] = "true", } -- OpenCode Zen's free tier is gated on a client fingerprint, not on an account: -- 1) User-Agent whose leading token is opencode/ -- 2) an x-opencode-session shaped ses_<12 lowercase hex><14 alnum> -- 3) stream:true in the body -- 4) at least five of OpenCode's own tool names in tools[] (bash/edit/glob/grep/read) -- The free models accept "Authorization: Bearer public", so no key is required. API.OPENCODE_UA = "opencode/1.18.31 ai-sdk/provider-utils/4.0.38 runtime/bun/1.3.14" function newOpencodeSession() local hex = "0123456789abcdef" local alnum = "abcdefghijklmnopqrstuvwxyz0123456789" local a, b = {}, {} for _ = 1, 12 do local i = math.random(1, #hex) a[#a + 1] = hex:sub(i, i) end for _ = 1, 14 do local i = math.random(1, #alnum) b[#b + 1] = alnum:sub(i, i) end return "ses_" .. table.concat(a) .. table.concat(b) end API.OPENCODE_TOOLS = { { type = "function", ["function"] = { name = "bash", description = "Run a shell command.", parameters = { type = "object", properties = { command = { type = "string" } }, required = { "command" } } } }, { type = "function", ["function"] = { name = "edit", description = "Edit a file.", parameters = { type = "object", properties = { filePath = { type = "string" }, oldString = { type = "string" }, newString = { type = "string" } }, required = { "filePath" } } } }, { type = "function", ["function"] = { name = "glob", description = "Find files by pattern.", parameters = { type = "object", properties = { pattern = { type = "string" } }, required = { "pattern" } } } }, { type = "function", ["function"] = { name = "grep", description = "Search file contents.", parameters = { type = "object", properties = { pattern = { type = "string" } }, required = { "pattern" } } } }, { type = "function", ["function"] = { name = "read", description = "Read a file.", parameters = { type = "object", properties = { filePath = { type = "string" } }, required = { "filePath" } } } }, } local function opencodeSpoof(key) return { ["User-Agent"] = API.OPENCODE_UA, ["Accept"] = "text/event-stream", ["x-opencode-session"] = newOpencodeSession(), ["x-opencode-client"] = "cli", ["Authorization"] = "Bearer " .. ((key ~= nil and key ~= "") and key or "public"), } end -- Walk a Server-Sent-Events body and stitch the streamed deltas back together. function parseOpencodeStream(raw) local text, thinking = {}, {} local calls = {} local ok0, decoded0 = pcall(function() return HttpService:JSONDecode(raw) end) if ok0 and type(decoded0) == "table" and decoded0.choices then return nil end for line in tostring(raw):gmatch("[^\r\n]+") do local data = line:match("^data:%s*(.+)$") if data and data ~= "[DONE]" then local ok, chunk = pcall(function() return HttpService:JSONDecode(data) end) if ok and type(chunk) == "table" then if type(chunk.error) == "table" and chunk.error.message then error("OpenCode: " .. tostring(chunk.error.message)) end local choice = chunk.choices and chunk.choices[1] local delta = choice and choice.delta if type(delta) == "table" then if type(delta.content) == "string" then table.insert(text, delta.content) end if type(delta.reasoning) == "string" then table.insert(thinking, delta.reasoning) end if type(delta.reasoning_content) == "string" then table.insert(thinking, delta.reasoning_content) end if type(delta.tool_calls) == "table" then for _, tc in ipairs(delta.tool_calls) do if type(tc) == "table" then local idx = #calls + 1 if tonumber(tc.index) ~= nil then idx = tonumber(tc.index) + 1 end calls[idx] = calls[idx] or { id = "", name = "", args = "" } if tc.id then calls[idx].id = tc.id end local fn = tc["function"] if type(fn) == "table" then if fn.name then calls[idx].name = fn.name end if type(fn.arguments) == "string" then calls[idx].args = calls[idx].args .. fn.arguments end end end end end end end end end local answer = table.concat(text) local reason = table.concat(thinking) local toolCalls = nil if #calls > 0 then toolCalls = {} for _, c in ipairs(calls) do if c.name ~= "" then local args = {} if c.args ~= "" then local okA, dec = pcall(function() return HttpService:JSONDecode(c.args) end) if okA and type(dec) == "table" then args = dec end end local id = c.id ~= "" and c.id or ("call_" .. tostring(#toolCalls + 1)) table.insert(toolCalls, { id = id, name = c.name, args = args }) end end if #toolCalls == 0 then toolCalls = nil end end if toolCalls then return { text = answer, thinking = reason ~= "" and reason or nil, searches = {}, toolCalls = toolCalls } end if answer == "" and reason ~= "" then return { text = reason, thinking = nil, searches = {} } end if answer == "" then error("The free model returned an empty stream. Try another free model.") end return { text = answer, thinking = reason ~= "" and reason or nil, searches = {} } end local function requestHttp(url, method, headers, body, spoof) local fn = (syn and syn.request) or http_request or request if type(fn) ~= "function" then error("This executor has no HTTP request function. Use an executor that supports request/http_request.") end local merged = {} for k, v in pairs(spoof or API.CODEX_SPOOF) do merged[k] = v end for k, v in pairs(headers or {}) do merged[k] = v end local ok, response, extra = pcall(fn, { Url = url, Method = method, Headers = merged, Body = body or "", Timeout = 90, }) if not ok then error("Executor HTTP failure: " .. tostring(response):sub(1, 350)) end if type(response) ~= "table" then local wrapped pcall(function() wrapped = { StatusCode = response.StatusCode, Body = response.Body } end) if type(wrapped) == "table" and (wrapped.StatusCode ~= nil or wrapped.Body ~= nil) then response = wrapped elseif type(response) == "string" then response = { StatusCode = 200, Body = response } else local detail = type(response) .. ": " .. tostring(response):sub(1, 120) if extra ~= nil then detail = detail .. " | " .. tostring(extra):sub(1, 160) end error("Executor returned an unreadable HTTP response (" .. detail .. ")") end end local status = tonumber(response.StatusCode or response.Status or response.status_code or response.status) or 0 local responseBody = response.Body or response.body or response.response or "" if status == 0 and tostring(responseBody) ~= "" then status = 200 end if status < 200 or status >= 300 then local detail = tostring(responseBody):gsub("[%c]", " ") local okDecode, decoded = pcall(function() return HttpService:JSONDecode(responseBody) end) if okDecode and type(decoded) == "table" then local err = decoded.error if type(err) == "table" then detail = tostring(err.message or err.msg or err.code or responseBody) elseif err ~= nil then detail = tostring(err) elseif decoded.msg ~= nil then detail = tostring(decoded.msg) elseif decoded.message ~= nil then detail = tostring(decoded.message) end end if detail == "" then detail = "(empty response body)" end local freeLimit = detail:lower() if freeLimit:find("free usage limit", 1, true) or freeLimit:find("freeusagelimit", 1, true) or freeLimit:find("only be used from within opencode", 1, true) or freeLimit:find("freetiererror", 1, true) then error("OPENCODE_FREE_TIER:" .. detail:sub(1, 200)) end if status == 401 then error("Authentication failed (HTTP 401). Check your API key for the selected provider. Response: " .. detail:sub(1, 350)) end if status == 402 then if url:find("openrouter.ai", 1, true) then error("HTTP 402: OpenRouter says this request is not covered by your credits. Response: " .. detail:sub(1, 250)) end error("HTTP 402: AgentRouter's premium-model batch quota is empty. This is separate from your account balance/credits. " .. "Claude Opus 5.5 and GPT-6.x refill daily at 10:00 and 19:00 Beijing time. DeepSeek V4 Flash is unlimited.") end if status == 403 then local dl = detail:lower() if dl:find("freetier") or dl:find("free tier") or dl:find("within opencode") then error("OPENCODE_FREE_TIER") end if dl:find("unauthorized client") then error("AgentRouter rejected this client's fingerprint (unauthorized_client_error). Claude models require the Claude Code headers; other models require the Codex headers.") end error("Access denied (HTTP 403). " .. detail:sub(1, 200)) end if status == 400 then local dl = detail:lower() if dl:find("content%-blocked") or dl:find("content_blocked") or dl:find("content blocked") or dl:find("blocked by the provider") or dl:find("content filter") or dl:find("sensitive content") then error("CONTENT_BLOCKED|" .. detail:sub(1, 200)) end if dl:find("/v1/responses") or dl:find("responses") then error("NEED_RESPONSES_API:" .. detail:sub(1, 200)) end error("Bad request (HTTP 400): " .. detail:sub(1, 250)) end if status == 404 then error("Not found (HTTP 404). Endpoint or model ID is invalid. Response: " .. detail:sub(1, 350)) end if status == 429 then error("Rate limited (HTTP 429). Wait and retry, or switch model. Response: " .. detail:sub(1, 300)) end if status >= 500 then local dl = detail:lower() if dl:find("无可用渠道", 1, true) or dl:find("no available channel", 1, true) or dl:find("no channel available", 1, true) then error("NO_CHANNEL|" .. detail:sub(1, 300)) end if dl:find("sensitive words") or dl:find("sensitive content") or dl:find("content_blocked") then error("CONTENT_BLOCKED|" .. detail:sub(1, 200)) end error("Server error (HTTP " .. tostring(status) .. "). Temporary failure — retry later. Response: " .. detail:sub(1, 300)) end error("HTTP " .. tostring(status) .. ": " .. detail:sub(1, 500)) end return responseBody, status end API.modelCache = { at = 0, ids = nil, missAt = 0 } API.blockedModels = {} API.lastGoodModel = nil API.agentRouterModels = function(key) local now = os.time() if API.modelCache.ids and now - API.modelCache.at < 300 then return API.modelCache.ids end if not API.modelCache.ids and now - (API.modelCache.missAt or 0) < 60 then return nil end if key == nil or key == "" then return nil end local ok, raw = pcall(function() return requestHttp("https://agentrouter.org/v1/models", "GET", { ["Accept"] = "application/json", ["Authorization"] = "Bearer " .. key, }) end) if not ok then API.modelCache.missAt = now return nil end local okDecode, decoded = pcall(function() return HttpService:JSONDecode(raw) end) if not okDecode or type(decoded) ~= "table" then API.modelCache.missAt = now return nil end local ids = {} for _, item in ipairs(decoded.data or decoded.models or {}) do local id = type(item) == "table" and (item.id or item.model or item.name) or nil if type(id) == "string" and id ~= "" then ids[#ids + 1] = id end end if #ids == 0 then return nil end API.modelCache.at, API.modelCache.ids = now, ids return ids end API.preferredModel = function(ids) if type(ids) ~= "table" or #ids == 0 then return nil end local have = {} for _, id in ipairs(ids) do have[id] = true end for _, id in ipairs({ "gpt-6.1-sol", "gpt-6-astra", "gpt-5.6-sol" }) do if have[id] then return id end end for _, m in ipairs(MODELS) do if m.format == "openai" and have[m.id] then return m.id end end for _, id in ipairs(ids) do local lower = id:lower() if not lower:find("embed") and not lower:find("whisper") and not lower:find("tts") and not lower:find("image") and not lower:find("dall") and not lower:find("moderation") then return id end end return nil end do local BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" function urlEncode(s) return (tostring(s):gsub("[^%w]", function(c) return string.format("%%%02X", string.byte(c)) end)) end function urlDecode(s) return (tostring(s):gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end)) end local function decodeEntities(s) s = s:gsub("<", "<"):gsub(">", ">"):gsub(""", '"'):gsub("'", "'"):gsub("'", "'") s = s:gsub(" ", " ") return (s:gsub("&", "&")) end local BLOCK_TAGS = { p = true, div = true, li = true, ul = true, ol = true, dl = true, dt = true, dd = true, h1 = true, h2 = true, h3 = true, h4 = true, h5 = true, h6 = true, br = true, hr = true, tr = true, td = true, th = true, table = true, thead = true, tbody = true, tfoot = true, caption = true, section = true, article = true, header = true, footer = true, main = true, nav = true, aside = true, blockquote = true, pre = true, figure = true, figcaption = true, form = true, fieldset = true, legend = true, address = true, details = true, summary = true, center = true, colgroup = true } function stripHtml(s) local SC = "[Ss][Cc][Rr][Ii][Pp][Tt]" local ST = "[Ss][Tt][Yy][Ll][Ee]" s = s:gsub("<" .. SC .. "[^>]*>.-]", " ") s = s:gsub("<" .. ST .. "[^>]*>.-]", " ") s = s:gsub("", " ") s = s:gsub("<[^>]+>", function(tag) local name = tag:match("^([^<]*)', pos) if not s then break end href = href:gsub("&", "&") local u = href:match("uddg=([^&]+)") u = u and urlDecode(u) or nil if u and u:match("^https?://") then local snip = "" local snipS, _, rawSnip = body:find('class="result__snippet"[^>]*>(.-)', e + 1) if snipS and rawSnip then snip = decodeEntities((rawSnip:gsub("<[^>]+>", ""))) end results[#results + 1] = { url = u, title = decodeEntities(title), snippet = snip } end pos = e + 1 end if #results == 0 then return nil, "no results (page blocked or format changed)" end local lines = {} for i, r in ipairs(results) do lines[#lines + 1] = i .. ". " .. r.title lines[#lines + 1] = " " .. r.url if r.snippet ~= "" then lines[#lines + 1] = " " .. r.snippet end end return table.concat(lines, "\n") end webFetch = function(url) if not url:match("^https?://") then if url:match("^%a[%w+.-]*://") then return nil, "only http(s) URLs can be read" end url = "https://" .. url end local ok, body = pcall(requestHttp, url, "GET", { ["User-Agent"] = BROWSER_UA, ["Accept"] = "text/html,application/xhtml+xml,text/plain,*/*" }, "", {}) if not ok then return nil, tostring(body):sub(1, 200) end if type(body) ~= "string" then return nil, "unreadable response" end local out = stripHtml(body) if #out > 6000 then out = out:sub(1, 6000) while #out > 0 and out:byte(-1) >= 128 do out = out:sub(1, -2) end out = out .. "\n... (truncated)" end if out == "" then return nil, "page had no readable text" end return out end end local function requestOpenAICompatible(url, payload, headers, spoof) local candidate = table.clone(payload) local changed = {} for attempt = 1, 4 do local ok, result = pcall(function() return requestHttp(url, "POST", headers, HttpService:JSONEncode(candidate), spoof) end) if ok then return result end local message = tostring(result) local lower = message:lower() local unsupported = lower:find("unsupported", 1, true) or lower:find("not support", 1, true) or lower:find("unknown parameter", 1, true) or lower:find("unrecognized parameter", 1, true) or lower:find("invalid parameter", 1, true) or lower:find("invalid value", 1, true) if not unsupported then error(message) end if lower:find("tool_choice", 1, true) and candidate.tool_choice ~= nil and not changed.tool_choice then candidate.tool_choice = nil changed.tool_choice = true elseif lower:find("reasoning_effort", 1, true) and candidate.reasoning_effort ~= nil and not changed.reasoning_effort then candidate.reasoning_effort = nil changed.reasoning_effort = true elseif lower:find("max_tokens", 1, true) and candidate.max_tokens ~= nil and not changed.max_tokens then candidate.max_completion_tokens = candidate.max_tokens candidate.max_tokens = nil changed.max_tokens = true elseif lower:find("max_completion_tokens", 1, true) and candidate.max_completion_tokens ~= nil and not changed.max_completion_tokens then candidate.max_tokens = candidate.max_completion_tokens candidate.max_completion_tokens = nil changed.max_completion_tokens = true else if lower:find("model", 1, true) and (lower:find("not supported", 1, true) or lower:find("unsupported", 1, true) or lower:find("not found", 1, true)) then error(message .. "\nCheck the model ID against models available for your key.") end error(message) end end error("Request rejected after compatibility retries.") end local function apiKeyValue(value) local key = tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "") key = key:gsub('^"(.*)"$', "%1"):gsub("^'(.*)'$", "%1") key = key:gsub("^[Bb][Ee][Aa][Rr][Ee][Rr]%s+", "") return key:gsub("^%s+", ""):gsub("%s+$", "") end API.CATALOGUES = { opencode = { title = "OpenCode Zen models", blurb = "Search the free Zen catalogue", url = API.OPENCODE_MODELS_URL, provider = "opencode", spoof = {}, accent = Color3.fromRGB(60, 130, 255), brand = "OpenCode Zen", missing = "Zen free models still answer; a key unlocks the paid ones.", }, agentrouter = { title = "AgentRouter models", blurb = "Search the models this key can reach", url = API.AGENTROUTER_MODELS_URL, provider = "agentrouter", spoof = API.CODEX_SPOOF, accent = Color3.fromRGB(77, 107, 254), brand = "AgentRouter", missing = "Add an AgentRouter key in Settings \u{2192} Providers to load this list.", }, openrouter = { title = "OpenRouter models", blurb = "Search the live catalogue", url = API.OPENROUTER_MODELS_URL, provider = "openrouter", spoof = API.OPENROUTER_SPOOF, accent = Color3.fromRGB(120, 100, 240), brand = "OpenRouter", missing = "Add an OpenRouter key in Settings \u{2192} Providers to use these.", }, } API.CATALOGUE_ORDER = { "opencode", "agentrouter", "openrouter" } API.catalogueCache, API.catalogueWaiting, API.catalogueLoading = {}, {}, {} function API.catalogueKey(provider) if provider == "openrouter" then return apiKeyValue(settings.openrouterKey) end if provider == "opencode" then local key = apiKeyValue(settings.opencodeApiKey) if key == "" then key = apiKeyValue(OPENCODE_KEY) end return key end local key = apiKeyValue(settings.apiKey) if key == "" then key = apiKeyValue(AGENTROUTER_KEY) end return key end function API.prettifyId(id) local tail = tostring(id or ""):match("([^/]+)$") or tostring(id or "") local words = {} for word in tail:gmatch("[^%-_]+") do words[#words + 1] = word:sub(1, 1):upper() .. word:sub(2) end local text = table.concat(words, " ") if text == "" then return tostring(id or "") end return text end function API.parseCatalogue(provider) local spec = API.CATALOGUES[provider] if not spec then return nil, "Unknown provider." end local key = API.catalogueKey(provider) if provider == "agentrouter" and key == "" then return nil, spec.missing end local headers = { ["Accept"] = "application/json" } if key ~= "" then headers["Authorization"] = "Bearer " .. key end local function fetch(sent, spoof) local ok, result = pcall(requestHttp, spec.url, "GET", sent, "", spoof or {}) if ok then return result end return nil, tostring(result) end local raw, failure = fetch(headers) if not raw then local text = tostring(failure) if text:find("431", 1, true) or text:find("unreadable HTTP", 1, true) then local slim = {} if headers["Authorization"] then slim["Authorization"] = headers["Authorization"] end local slimRaw, slimFailure = fetch(slim) if slimRaw then raw = slimRaw else failure = tostring(slimFailure) end end end if not raw and type(spec.spoof) == "table" and next(spec.spoof) then local spoofRaw, spoofFailure = fetch(headers, spec.spoof) if spoofRaw then raw = spoofRaw else failure = tostring(spoofFailure) end end if not raw then local text = tostring(failure or "unknown error") if text:find("431", 1, true) then text = "the executor's HTTP layer sent headers the server refused (HTTP 431). Restart the executor and try again; if it keeps failing, this executor cannot load the catalogue." end return nil, text end local okDecode, decoded = pcall(function() return HttpService:JSONDecode(tostring(raw)) end) if not okDecode or type(decoded) ~= "table" then return nil, spec.title .. " sent a list this could not read." end local rows = type(decoded.data) == "table" and decoded.data or (type(decoded.models) == "table" and decoded.models or nil) if not rows then return nil, spec.title .. " sent a list this could not read." end local list = {} for _, item in ipairs(rows) do local id = "" if type(item) == "table" then id = tostring(item.id or item.model or item.name or "") end if id ~= "" and not (provider == "opencode" and id == "test") then local price = type(item.pricing) == "table" and item.pricing or {} local arch = type(item.architecture) == "table" and item.architecture or {} local inputs = type(arch.input_modalities) == "table" and arch.input_modalities or {} local vision = false for _, mode in ipairs(inputs) do if tostring(mode) == "image" then vision = true break end end local free if price.prompt ~= nil or price.completion ~= nil then free = tostring(price.prompt or "1") == "0" and tostring(price.completion or "1") == "0" elseif provider == "opencode" then free = id:find("free", 1, true) ~= nil end local name = item.name or item.display_name if not name and provider == "opencode" then name = API.prettifyId((id:gsub("%-free$", ""))) end if vision == false and id:find("%-vl", 1, true) then vision = true end list[#list + 1] = { id = id, name = tostring(name or API.prettifyId(id)), ctx = tonumber(item.context_length or item.context) or 0, vision = vision, free = free } end end if #list == 0 then return nil, spec.title .. " returned no models." end table.sort(list, function(a, b) if (a.free == true) ~= (b.free == true) then return a.free == true end return a.name:lower() < b.name:lower() end) return list end function API.loadCatalogue(provider, callback) local cached = API.catalogueCache[provider] if cached then callback(cached) return end local waiting = API.catalogueWaiting[provider] or {} API.catalogueWaiting[provider] = waiting table.insert(waiting, callback) if API.catalogueLoading[provider] then return end API.catalogueLoading[provider] = true task.spawn(function() local list, failure = API.parseCatalogue(provider) API.catalogueLoading[provider] = false if list then API.catalogueCache[provider] = list end local queued = API.catalogueWaiting[provider] or {} API.catalogueWaiting[provider] = {} for _, fn in ipairs(queued) do pcall(fn, list, failure) end end) end function API.openRouterCatalog(callback) API.loadCatalogue("openrouter", callback) end function API.catalogueDescriptor(provider, entry) local spec = API.CATALOGUES[provider] or API.CATALOGUES.openrouter local id = tostring(entry.id or "") local name = tostring(entry.name or id) local short = id:match("([^/]+)$") or name local desc if provider == "openrouter" then if entry.free then desc = "OpenRouter · free" elseif (tonumber(entry.ctx) or 0) >= 1000 then desc = "OpenRouter · " .. tostring(math.floor(tonumber(entry.ctx) / 1000)) .. "k context" else desc = "OpenRouter · paid" end elseif provider == "opencode" then desc = entry.free and "OpenCode Zen · free" or "OpenCode Zen" else desc = entry.free and "AgentRouter · free" or "AgentRouter" end local accent, format = spec.accent, "openai" if provider == "agentrouter" then if id:find("claude", 1, true) then accent, format = Color3.fromRGB(205, 120, 80), "anthropic" elseif id:find("gpt", 1, true) then accent = Color3.fromRGB(16, 163, 127) end end return { brand = provider, brandName = spec.brand, name = name, short = short, id = id, format = format, provider = provider, endpoint = "chat", wire = "chat", mark = string.upper(short:sub(1, 2)), accent = accent, desc = desc, think = "Think", vision = entry.vision == true } end function API.openRouterDescriptor(entry) return API.catalogueDescriptor("openrouter", entry) end function API.useCatalogueModel(provider, entry) local spec = API.CATALOGUES[provider] or API.CATALOGUES.openrouter local index for i, m in ipairs(MODELS) do if m.id == entry.id and (m.provider or "agentrouter") == spec.provider then index = i end end if not index then MODELS[#MODELS + 1] = API.catalogueDescriptor(provider, entry) index = #MODELS end modelIndex = index settings.modelId = tostring(entry.id or "") settings.customModel = { provider = provider, id = tostring(entry.id or ""), name = tostring(entry.name or entry.id or ""), ctx = tonumber(entry.ctx) or 0, vision = entry.vision == true, free = entry.free == true } saveSettings() applyModel() end function API.useOpenRouterModel(entry) API.useCatalogueModel("openrouter", entry) end function API.restoreCatalogueModel() local saved = settings.customModel if type(saved) ~= "table" or type(saved.id) ~= "string" or saved.id == "" then return end local provider = API.CATALOGUES[saved.provider] and saved.provider or "openrouter" local index for i, m in ipairs(MODELS) do if m.id == saved.id and (m.provider or "") == provider then index = i end end if not index then MODELS[#MODELS + 1] = API.catalogueDescriptor(provider, saved) index = #MODELS end modelIndex = index end function API.restoreOpenRouterModel() API.restoreCatalogueModel() end API.restoreCatalogueModel() function closeCataloguePicker() if openRouterModal then openRouterModal:Destroy() openRouterModal = nil end end function closeOpenRouterPicker() closeCataloguePicker() end function openCataloguePicker(provider) local spec = API.CATALOGUES[provider] or API.CATALOGUES.openrouter closeCataloguePicker() local layer = new("Frame", { Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, ZIndex = 90 }, window) openRouterModal = layer local backdrop = button({ Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.bg, BackgroundTransparency = 0.45, ZIndex = 90 }, layer) local box = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(400, 400), BackgroundColor3 = C.panel, ZIndex = 91 }, layer) round(box, 14) stroke(box) text({ Position = UDim2.fromOffset(16, 12), Size = UDim2.new(1, -48, 0, 20), Text = spec.title, Font = Enum.Font.GothamBold, TextSize = 15, ZIndex = 92 }, box) local shut = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -10, 0, 10), Size = UDim2.fromOffset(24, 24), BackgroundTransparency = 1, ZIndex = 92 }, box) icon("cross", shut, C.subtext, 13) shut.MouseButton1Click:Connect(closeCataloguePicker) local search = new("TextBox", { Position = UDim2.fromOffset(12, 40), Size = UDim2.new(1, -24, 0, 32), BackgroundColor3 = C.hover, PlaceholderText = "Search by name or id", PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = 12, Text = "", ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 92 }, box) round(search, 8) pad(search, 0, 10, 0, 10) local note = text({ Position = UDim2.fromOffset(14, 78), Size = UDim2.new(1, -28, 0, 16), Text = "Loading the catalogue\u{2026}", TextSize = 11, TextColor3 = C.subtext, ZIndex = 92 }, box) local list = new("ScrollingFrame", { Position = UDim2.fromOffset(8, 98), Size = UDim2.new(1, -16, 1, -106), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 92 }, box) new("UIListLayout", { Padding = UDim.new(0, 2) }, list) local entries = nil local function render() for _, child in ipairs(list:GetChildren()) do if child:IsA("GuiObject") then child:Destroy() end end if not entries then return end local query = tostring(search.Text or ""):lower() local shown, total = 0, 0 for _, entry in ipairs(entries) do total = total + 1 local hit = query == "" or entry.name:lower():find(query, 1, true) ~= nil or entry.id:lower():find(query, 1, true) ~= nil if hit and shown < 120 then shown = shown + 1 local item = button({ Size = UDim2.new(1, 0, 0, 46), BackgroundColor3 = C.hover, BackgroundTransparency = 1, LayoutOrder = shown, ZIndex = 93 }, list) round(item, 8) hover(item, C.hover, 0.1) text({ Position = UDim2.fromOffset(10, 4), Size = UDim2.new(1, -20, 0, 16), Text = entry.name, TextSize = 12, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 94 }, item) local meta = entry.id if entry.free then meta = meta .. " · free" end if entry.vision then meta = meta .. " · vision" end text({ Position = UDim2.fromOffset(10, 22), Size = UDim2.new(1, -20, 0, 14), Text = meta, TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 94 }, item) item.MouseButton1Click:Connect(function() API.useCatalogueModel(provider, entry) closeCataloguePicker() end) end end local freeCount = 0 for _, entry in ipairs(entries) do if entry.free then freeCount = freeCount + 1 end end if shown < total then note.Text = "Showing " .. tostring(shown) .. " of " .. tostring(total) .. " · type to narrow" elseif freeCount > 0 then note.Text = tostring(total) .. " models · " .. tostring(freeCount) .. " free" else note.Text = tostring(total) .. " models" end end local function warn() if API.catalogueKey(provider) == "" and provider ~= "opencode" then note.Text = spec.missing note.TextColor3 = C.error end end warn() search:GetPropertyChangedSignal("Text"):Connect(render) backdrop.MouseButton1Click:Connect(closeCataloguePicker) API.loadCatalogue(provider, function(found, failure) if not openRouterModal then return end if found then entries = found note.TextColor3 = C.subtext render() warn() elseif API.catalogueKey(provider) == "" and provider ~= "opencode" then note.Text = spec.missing note.TextColor3 = C.error else note.Text = "Could not load the catalogue: " .. tostring(failure or "unknown error"):sub(1, 200) note.TextColor3 = C.error end end) end function openOpenRouterPicker() openCataloguePicker("openrouter") end local function arKeyList() local primary = apiKeyValue(settings.apiKey) local legacy = apiKeyValue(AGENTROUTER_KEY) local list = {} if primary ~= "" then list[1] = primary end if legacy ~= "" and legacy ~= primary then list[#list + 1] = legacy end return list end local function arKeyValue() return arKeyList()[1] or "" end API.describeError = function(err) local message = tostring(err) for _ = 1, 3 do message = message:gsub("^.-:%d+:%s*", "") end local needsResponses = message:find("NEED_RESPONSES_API", 1, true) ~= nil or message:find("/v1/responses", 1, true) ~= nil message = message:gsub("^OPENCODE_FREE_TIER:?%s*", "") message = message:gsub("^NEED_RESPONSES_API:?%s*", "") if needsResponses then return "This model only answers on the Responses API, and the request did not use it. " .. "Retry, or pick a different model." end local blocked = message:match("^CONTENT_BLOCKED|(.*)$") if blocked then return "The provider blocked this request as sensitive content. Response: " .. blocked:sub(1, 200) end local detail = message:match("^NO_CHANNEL|(.*)$") if not detail then return message:sub(1, 300) end local group = detail:match("分组%s*([%w%-_]+)") or detail:match("group%s*([%w%-_]+)") or "default" if arKeyValue() == "" then return message:sub(1, 300) end local ids, listing = {}, "none listed for this key" for _, key in ipairs(arKeyList()) do local tried = API.agentRouterModels(key) if tried and #tried > 0 then ids = tried break end end local shown = {} for i = 1, math.min(#ids, 10) do shown[i] = tostring(ids[i]) end if #shown > 0 then listing = table.concat(shown, ", ") .. (#ids > 10 and (" (+" .. tostring(#ids - 10) .. " more)") or "") end return "AgentRouter has no available channel for this model in your key's group (" .. group .. ").\n" .. "Models this key can use: " .. listing .. ".\n" .. "Pick one of those in the model menu, or check the token's group in the AgentRouter dashboard.\n" .. "If the list is empty, the token may be invalid or its group may have no channels." end local function decodeJson(raw, label) local ok, decoded = pcall(function() return HttpService:JSONDecode(raw) end) if not ok or type(decoded) ~= "table" then error(label .. " returned invalid JSON.") end return decoded end local function modelProvider(model) if model and model.provider then return model.provider end if model and model.brand == "opencode" then return "opencode" end return settings.provider or "agentrouter" end function getActiveKey(forModel) local prov = modelProvider(forModel or MODELS[modelIndex]) if prov == "openrouter" then local k = apiKeyValue(settings.openrouterKey) return k, "OpenRouter" elseif prov == "opencode" then local k = apiKeyValue(settings.opencodeApiKey) if k == "" then k = apiKeyValue(OPENCODE_KEY) end return k, "OpenCode" else local k = apiKeyValue(settings.apiKey) if k == "" then k = apiKeyValue(AGENTROUTER_KEY) end return k, "AgentRouter" end end testApiConnection = function(key, callback, forceProvider) key = apiKeyValue(key) local testModel = MODELS[modelIndex] local testOpenCode = forceProvider == "opencode" or modelProvider(testModel) == "opencode" if key == "" and not testOpenCode then callback(false, "Paste an API key first") return end if forceProvider == "openrouter" then task.spawn(function() local ok, response = pcall(function() return requestHttp(API.OPENROUTER_KEY_URL, "GET", { ["Content-Type"] = "application/json", ["Accept"] = "application/json", ["Authorization"] = "Bearer " .. key, }, "", API.OPENROUTER_SPOOF) end) if ok then callback(true, "Connected · key accepted") else callback(false, tostring(response):sub(1, 350)) end end) return end task.spawn(function() local ok, result = pcall(function() local model = MODELS[modelIndex] if forceProvider == "opencode" and model.provider ~= "opencode" then model = { id = "beam-free", format = "openai", provider = "opencode" } end local useOpenCode = forceProvider == "opencode" or modelProvider(model) == "opencode" local payload = { model = model.id, messages = {{ role = "user", content = "Reply only with OK." }}, max_tokens = 8, } local headers = { ["Content-Type"] = "application/json", ["Accept"] = "application/json", } if model.format == "anthropic" and not useOpenCode then headers["x-api-key"] = key headers["Authorization"] = "Bearer " .. key headers["anthropic-version"] = "2023-06-01" payload = { model = model.id, max_tokens = 8, messages = {{ role = "user", content = "Reply only with OK." }}, } requestHttp(API.AGENTROUTER_MESSAGES_URL, "POST", headers, HttpService:JSONEncode(payload), API.CLAUDE_SPOOF) elseif model.endpoint == "responses" and not useOpenCode then headers["Authorization"] = "Bearer " .. key payload = { model = model.id, input = "Reply only with OK.", max_output_tokens = 8, } requestHttp(API.AGENTROUTER_RESPONSES_URL, "POST", headers, HttpService:JSONEncode(payload)) elseif useOpenCode then payload.stream = true payload.tools = API.OPENCODE_TOOLS payload.tool_choice = "none" headers["Accept"] = "text/event-stream" headers["Authorization"] = nil local ok1 = pcall(function() requestOpenAICompatible(API.OPENCODE_URL, payload, headers, opencodeSpoof(key)) end) if not ok1 then local ok2 = pcall(function() requestOpenAICompatible(API.OPENCODE_URL, payload, headers, opencodeSpoof("public")) end) if not ok2 then requestOpenAICompatible(API.OPENCODE_INFERENCE_URL, payload, headers, opencodeSpoof("public")) end end else headers["Authorization"] = "Bearer " .. key local ok1, err1 = pcall(function() requestOpenAICompatible(API.AGENTROUTER_URL, payload, headers) end) if not ok1 then error(err1) end end return "Connected · model responded" end) if ok then callback(true, result) else local message = tostring(result):gsub("^.-:%d+:%s*", "") callback(false, message:sub(1, 350)) end end) end do local B64A = {} for index = 1, 64 do B64A[index - 1] = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"):sub(index, index) end local function hexColour(colour) local ok, r, g, b = pcall(function() return math.round(colour.R * 255), math.round(colour.G * 255), math.round(colour.B * 255) end) if not ok then return "unknown" end return string.format("#%02X%02X%02X", r, g, b) end local function avatarLines(player) local lines = {"Player: " .. player.DisplayName .. " (@" .. player.Name .. ", UserId " .. tostring(player.UserId) .. ", account age " .. tostring(player.AccountAge or 0) .. " days)"} local char = player.Character local humanoid = char and char:FindFirstChildOfClass("Humanoid") if humanoid then lines[#lines + 1] = "Rig: " .. tostring(humanoid.RigType):gsub("RigType%.", "") .. ", health " .. tostring(math.floor(humanoid.Health)) .. "/" .. tostring(math.floor(humanoid.MaxHealth)) .. ", walk speed " .. tostring(humanoid.WalkSpeed) local scales = {} for _, key in ipairs({ "HeightScale", "WidthScale", "DepthScale", "HeadScale", "BodyTypeScale", "ProportionScale" }) do local ok, value = pcall(function() return humanoid[key] end) if ok and type(value) == "number" then scales[#scales + 1] = key:gsub("Scale", "") .. " " .. string.format("%.2f", value) end end if #scales > 0 then lines[#lines + 1] = "Body scales: " .. table.concat(scales, ", ") end end local colours = char and char:FindFirstChild("BodyColors") if colours then local parts = {} for _, key in ipairs({ "HeadColor3", "TorsoColor3", "LeftArmColor3", "RightArmColor3", "LeftLegColor3", "RightLegColor3" }) do local ok, value = pcall(function() return colours[key] end) if ok and typeof(value) == "Color3" then parts[#parts + 1] = key:gsub("Color3", ""):gsub("(%u)", " %1") .. " " .. hexColour(value) end end if #parts > 0 then lines[#lines + 1] = "Body colours:" .. table.concat(parts, ",") end end local worn, tools = {}, {} for _, item in ipairs(char and char:GetDescendants() or {}) do if item:IsA("Accessory") then worn[#worn + 1] = tostring(item.AccessoryType) .. ": " .. item.Name elseif item:IsA("Shirt") then worn[#worn + 1] = "Shirt " .. tostring(item.ShirtTemplate) elseif item:IsA("Pants") then worn[#worn + 1] = "Pants " .. tostring(item.PantsTemplate) elseif item:IsA("ShirtGraphic") then worn[#worn + 1] = "T-shirt " .. tostring(item.Graphic) elseif item:IsA("Tool") then tools[#tools + 1] = item.Name end end lines[#lines + 1] = #worn > 0 and ("Wearing (" .. tostring(#worn) .. "): " .. table.concat(worn, ", ")) or "Wearing: nothing equipped" if #tools > 0 then lines[#lines + 1] = "Holding: " .. table.concat(tools, ", ") end local okDesc, desc = pcall(function() return Players:GetHumanoidDescriptionFromUserId(player.UserId) end) if okDesc and typeof(desc) == "Instance" then local ids = {} for _, key in ipairs({ "HairAccessory", "FaceAccessory", "NeckAccessory", "ShoulderAccessory", "FrontAccessory", "BackAccessory", "WaistAccessory", "HatAccessory", "Face", "Head" }) do local ok, value = pcall(function() return desc[key] end) if ok and type(value) == "number" and value > 0 then ids[#ids + 1] = key .. " " .. tostring(value) end end if #ids > 0 then lines[#lines + 1] = "Avatar asset ids: " .. table.concat(ids, ", ") end end return lines end local function sceneLines() local lines = {} local lp = Players.LocalPlayer lines[#lines + 1] = "Place: '" .. tostring(gameName) .. "' (PlaceId " .. tostring(placeId) .. "), " .. tostring(#(Players:GetPlayers())) .. " player(s) online." local char = lp and lp.Character local humanoid = char and char:FindFirstChildOfClass("Humanoid") if char then local okPivot, pivot = pcall(function() return char:GetPivot().Position end) if okPivot and typeof(pivot) == "Vector3" then lines[#lines + 1] = "Your character stands at (" .. tostring(math.floor(pivot.X)) .. ", " .. tostring(math.floor(pivot.Y)) .. ", " .. tostring(math.floor(pivot.Z)) .. ") in " .. tostring(char.Parent and char.Parent.Name or "workspace") .. "." end end if humanoid then local states = {} for _, state in ipairs({ "Running", "Jumping", "Climbing", "Swimming", "Dead" }) do local ok, value = pcall(function() return humanoid:GetState() == Enum.HumanoidStateType[state] end) if ok and value then states[#states + 1] = state end end if #states > 0 then lines[#lines + 1] = "State: " .. table.concat(states, ", ") .. "." end end local others = {} for _, other in ipairs(Players:GetPlayers()) do if other ~= lp then local otherChar = other.Character local otherHum = otherChar and otherChar:FindFirstChildOfClass("Humanoid") local detail = other.DisplayName if humanoid and otherHum and char then local okDist, dist = pcall(function() return math.floor((otherChar:GetPivot().Position - char:GetPivot().Position).Magnitude) end) if okDist then detail = detail .. " " .. tostring(dist) .. " studs away" end end if otherHum then detail = detail .. " (" .. tostring(math.floor(otherHum.Health)) .. " hp)" end others[#others + 1] = detail end end lines[#lines + 1] = #others > 0 and ("Nearby players: " .. table.concat(others, "; ")) or "No other players in this server." local camera = workspace.CurrentCamera if camera then local okCam, camPos = pcall(function() return camera.CFrame.Position end) if okCam then lines[#lines + 1] = "Camera sits at (" .. tostring(math.floor(camPos.X)) .. ", " .. tostring(math.floor(camPos.Y)) .. ", " .. tostring(math.floor(camPos.Z)) .. ")." end end local okClock, clock = pcall(function() return Lighting.ClockTime end) if okClock then lines[#lines + 1] = "Time of day: " .. string.format("%.1f", clock) .. "h." end local guis = {} local okGui, playerGui = pcall(function() return lp and lp:FindFirstChildOfClass("PlayerGui") end) if okGui and playerGui then for _, guiItem in ipairs(playerGui:GetChildren()) do if guiItem:IsA("ScreenGui") and guiItem.Enabled then guis[#guis + 1] = guiItem.Name end end end if #guis > 0 then lines[#lines + 1] = "On-screen interfaces: " .. table.concat(guis, ", ") end return lines end local briefText, briefAt, briefTools = nil, 0, nil function API.contextBrief(withTools) if briefText and briefTools == withTools and os.clock() - briefAt < 20 then return briefText end local ok, text = pcall(function() local lp = Players.LocalPlayer if not lp then return nil end local bits = { "The user is " .. lp.DisplayName .. " (@" .. lp.Name .. ")." } local char = lp.Character local humanoid = char and char:FindFirstChildOfClass("Humanoid") if humanoid then local items = 0 for _, item in ipairs(char:GetDescendants()) do if item:IsA("Accessory") or item:IsA("Shirt") or item:IsA("Pants") or item:IsA("ShirtGraphic") then items = items + 1 end end bits[#bits + 1] = "They play as a " .. tostring(humanoid.RigType):gsub("RigType%.R", "R") .. " character wearing " .. tostring(items) .. " avatar items." end bits[#bits + 1] = tostring(#(Players:GetPlayers())) .. " player(s) are online with them." if withTools then bits[#bits + 1] = "You can look at their avatar or the scene around them with inspect_player." end return table.concat(bits, " ") end) briefText, briefAt, briefTools = ok and text or nil, os.clock(), withTools return briefText or "" end function API.playerReport(view) local ok, text = pcall(function() if view == "scene" then return table.concat(sceneLines(), "\n") end local lp = Players.LocalPlayer if not lp then return "No local player is available here, so there is no avatar to read." end return table.concat(avatarLines(lp), "\n") end) if not ok then return "Could not read the character: " .. tostring(text) end return text end function API.avatarImage() local lp = Players.LocalPlayer if not lp then return nil, "No local player is available here." end local url = "https://thumbnails.roblox.com/v1/users/avatar?userIds=" .. tostring(lp.UserId) .. "&size=420x420&format=Png&isCircular=false" local okJson, body = pcall(requestHttp, url, "GET", { ["Accept"] = "application/json" }, "", {}) if not okJson then return nil, tostring(body) end local okDecode, data = pcall(function() return HttpService:JSONDecode(tostring(body)) end) local imageUrl if okDecode and type(data) == "table" and type(data.data) == "table" then for _, item in ipairs(data.data) do if type(item) == "table" and type(item.imageUrl) == "string" then imageUrl = item.imageUrl end end end if not imageUrl then return nil, "Roblox returned no avatar image for this account." end local okPng, png = pcall(requestHttp, imageUrl, "GET", {}, "", {}) if not okPng then return nil, tostring(png) end if type(png) ~= "string" or png:sub(2, 4) ~= "PNG" then return nil, "The avatar image could not be downloaded." end local out, n = {}, 0 for index = 1, #png, 3 do local b1 = png:byte(index) or 0 local b2 = png:byte(index + 1) or 0 local b3 = png:byte(index + 2) or 0 local value = b1 * 65536 + b2 * 256 + b3 n += 1 out[n] = B64A[bit32.rshift(value, 18)] .. B64A[bit32.band(bit32.rshift(value, 12), 63)] .. (png:byte(index + 1) and B64A[bit32.band(bit32.rshift(value, 6), 63)] or "=") .. (png:byte(index + 2) and B64A[bit32.band(value, 63)] or "=") end return { base64 = table.concat(out), mediaType = "image/png" } end end local function getAIReply(history, model, opts, toolResults) local useOpenCode = modelProvider(model) == "opencode" local useOpenRouter = modelProvider(model) == "openrouter" local apiKey, providerName = getActiveKey(model) if apiKey == "" then if useOpenCode then apiKey = "public" elseif useOpenRouter then error("OpenRouter needs an API key. Open Settings → Providers, paste it in the OpenRouter key field, press Save changes, then Retry.") else error("Open Settings, paste your " .. providerName .. " API key, and click Save changes.") end end -- One canonical tool list, translated into each provider's own envelope below. local toolSchemas = (opts ~= nil and type(opts.tools) == "table") and opts.tools or nil local hasTools = toolSchemas ~= nil and #toolSchemas > 0 local hasWeb = false if hasTools then for _, t in ipairs(toolSchemas) do if t.name == "web_search" then hasWeb = true break end end end local function schemaOf(t) if type(t.schema) == "table" then return t.schema end local props, names = {}, {} for name, desc in pairs(t.parameters or {}) do props[name] = { type = "string", description = tostring(desc) } names[#names + 1] = name end if next(props) == nil then return { type = "object", properties = { note = { type = "string" } } } end return { type = "object", properties = props, required = names } end local systemPrompt = (opts and opts.system) or "You are a general-purpose assistant in a Roblox chat interface. Respond directly to the user's actual request in clear English. Give accurate, useful answers. For code requests, return complete usable code when appropriate. Do not claim to have inspected the live game unless provided with evidence." if hasTools then systemPrompt = systemPrompt .. " When a tool is available and genuinely useful, call it instead of guessing, but never fabricate tool output." end if hasWeb then systemPrompt = systemPrompt .. " You have live web access: use web_search before answering questions about current events, versions, prices, documentation, or anything you are unsure of, and web_fetch to read a page in full. When your answer relies on the web, cite each source inline as a markdown link: [title](https://example.com/page)." end systemPrompt = systemPrompt .. "\n\nContext: You are currently in the Roblox game '" .. gameName .. "' (PlaceId: " .. tostring(placeId) .. ")." local okBrief, brief = pcall(API.contextBrief, hasTools) if okBrief and type(brief) == "string" and brief ~= "" then systemPrompt = systemPrompt .. "\n" .. brief end if useOpenCode and not hasTools then systemPrompt = systemPrompt .. "\n\nAlways answer with plain text. Never call any tool or function." end if settings.personality ~= "" then systemPrompt ..= "\n\nUser personality instructions: " .. settings.personality end if currentProject and currentProject.context and currentProject.context ~= "" then systemPrompt ..= "\n\nProject context: " .. currentProject.context end local messages = {} for _, item in ipairs(history) do if (item.role == "user" or item.role == "assistant") and not item.isError and not (item.role == "assistant" and (item.text or "") == "") then local content = item.text or "" if item.role == "user" and item.images and #item.images > 0 then local parts = {} if content ~= "" then table.insert(parts, { type = "text", text = content }) end for _, image in ipairs(item.images) do if image.base64 then table.insert(parts, { type = "image_url", image_url = { url = "data:" .. (image.mediaType or "image/png") .. ";base64," .. image.base64 } }) end end content = parts end table.insert(messages, { role = item.role, content = content }) end end -- Append the assistant's tool-call turns and their results in order. if toolResults then for _, item in ipairs(toolResults) do if type(item) == "table" and item.role then table.insert(messages, item) end end end local selectedModel = model.id local last = history[#history] if last and last.images and #last.images > 0 and not model.vision and model.visionId then selectedModel = model.visionId end if not model.vision and model.visionId and toolResults then for _, item in ipairs(toolResults) do if item.role == "user" and type(item.content) == "table" then selectedModel = model.visionId end end end local raw local opencodeStream = useOpenCode local function parseToolCalls(message) if type(message.tool_calls) ~= "table" or #message.tool_calls == 0 then return nil end local list = {} for _, tc in ipairs(message.tool_calls) do if type(tc) == "table" then local fn = type(tc["function"]) == "table" and tc["function"] or {} local args = {} if type(fn.arguments) == "string" and fn.arguments ~= "" then local ok, decoded = pcall(function() return HttpService:JSONDecode(fn.arguments) end) if ok and type(decoded) == "table" then args = decoded end elseif type(fn.arguments) == "table" then args = fn.arguments end local id = tostring(tc.id or "") if id == "" then id = "call_" .. tostring(#list + 1) end table.insert(list, { id = id, name = tostring(fn.name or ""), args = args }) end end if #list == 0 then return nil end return list end local function parseChatAnswer(decoded) local choice = decoded.choices and decoded.choices[1] local message = choice and choice.message if not message then error(decoded.error and (decoded.error.message or decoded.error) or "No assistant message.") end local toolCalls = parseToolCalls(message) local answer = message.content if type(answer) == "table" then local parts = {} for _, part in ipairs(answer) do if type(part) == "table" and part.text then table.insert(parts, part.text) end end answer = table.concat(parts, "\n") end -- Reasoning models can spend every token on reasoning and return empty content. if (answer == nil or tostring(answer) == "") and type(message.reasoning_content) == "string" then answer = message.reasoning_content elseif (answer == nil or tostring(answer) == "") and type(message.reasoning) == "string" then answer = message.reasoning end if answer == nil or tostring(answer) == "" then if toolCalls then return { text = "", thinking = nil, searches = {}, toolCalls = toolCalls } end if choice and tostring(choice.finish_reason or "") == "content_filter" then error("The provider filtered this response. Rephrase the request or switch models.") end error("Empty assistant message. The model returned no text (it may have hit a token limit or been filtered). Try again or switch models.") end local thinking = nil if type(message.reasoning_content) == "string" and message.reasoning_content ~= tostring(answer) then thinking = message.reasoning_content elseif opts.think then thinking = "Reasoning mode was requested for this response." end return { text = tostring(answer), thinking = thinking, searches = {}, toolCalls = toolCalls } end local function parseResponsesAnswer(decoded) local calls = {} for _, item in ipairs(decoded.output or {}) do if type(item) == "table" and item.type == "function_call" and item.name then local args = {} if type(item.arguments) == "string" and item.arguments ~= "" then local okArgs, decArgs = pcall(function() return HttpService:JSONDecode(item.arguments) end) if okArgs and type(decArgs) == "table" then args = decArgs end end table.insert(calls, { id = tostring(item.call_id or item.id or ("call_" .. tostring(#calls + 1))), name = tostring(item.name), args = args }) end end if type(decoded.output_text) == "string" and decoded.output_text ~= "" then return { text = decoded.output_text, thinking = opts.think and "Reasoning mode was requested for this response." or nil, searches = {}, toolCalls = #calls > 0 and calls or nil } end local texts, thinking = {}, nil for _, item in ipairs(decoded.output or {}) do if type(item) == "table" then if item.type == "message" then for _, part in ipairs(item.content or {}) do if type(part) == "table" and (part.type == "output_text" or part.type == "text") and part.text then table.insert(texts, part.text) end end elseif item.type == "reasoning" then local bits = {} for _, s in ipairs(item.summary or {}) do if type(s) == "table" and s.text then table.insert(bits, s.text) elseif type(s) == "string" then table.insert(bits, s) end end if #bits > 0 then thinking = table.concat(bits, "\n") end end end end if #texts == 0 and #calls == 0 then error(decoded.error and (decoded.error.message or decoded.error) or "No response text.") end return { text = table.concat(texts, "\n"), thinking = thinking or (opts.think and "Reasoning mode was requested for this response." or nil), searches = {}, toolCalls = #calls > 0 and calls or nil } end -- AgentRouter releases Claude/GPT capacity in shared daily batches. When a batch is -- empty the gateway returns 402 even if the account still has credit. DeepSeek has -- unlimited capacity, so keep the conversation usable instead of hard-failing. local function isQuotaError(err) local s = tostring(err):lower() return s:find("http 402", 1, true) ~= nil or s:find("budget pool", 1, true) ~= nil or s:find("quota", 1, true) ~= nil end -- Keep text turns, drop image parts: the unlimited fallback model is text-only. local function textOnly(item) if item.role == "tool" or item.tool_calls then return nil end if type(item.content) == "table" then local bits = {} for _, part in ipairs(item.content) do if type(part) == "table" and part.type == "text" and part.text then table.insert(bits, part.text) end end if #bits == 0 then return nil end return { role = item.role, content = table.concat(bits, "\n") } end return item end local function noChannelError(err) return tostring(err):find("NO_CHANNEL", 1, true) ~= nil end local arKeys = arKeyList local arKey = arKeyValue local function isAvoided(avoid, id) if not avoid then return false end if type(avoid) == "string" then return id == avoid end if not avoid[id] then return false end return true end -- One ordered list feeds every automatic fallback: DeepSeek (the unlimited catch-all), -- then the models this key can actually reach. Models that already refused this -- conversation as sensitive content are skipped until the app restarts. local function fallbackModelList(avoid, firstId) local ordered = {} local function add(id) id = tostring(id or "") if id == "" or table.find(ordered, id) then return end ordered[#ordered + 1] = id end add(firstId) if API.lastGoodModel then add(API.lastGoodModel) end local okLive, liveIds = pcall(function() return API.agentRouterModels(arKey()) end) if okLive and type(liveIds) == "table" then local preferred = API.preferredModel(liveIds) if preferred then add(preferred) end for _, id in ipairs(liveIds) do if #ordered >= 5 then break end add(id) end end add("gpt-6.1-sol") local kept = {} for _, id in ipairs(ordered) do if not isAvoided(avoid, id) and not API.blockedModels[id] then kept[#kept + 1] = id end end if #kept == 0 then kept = ordered end return kept end local function responsesInput() local inputMsgs = {} for _, item in ipairs(messages) do if item.role == "tool" then table.insert(inputMsgs, { type = "function_call_output", call_id = tostring(item.tool_call_id or ""), output = tostring(item.content or "") }) elseif item.tool_calls then if item.content ~= nil and tostring(item.content) ~= "" then table.insert(inputMsgs, { role = "assistant", content = tostring(item.content) }) end for _, tc in ipairs(item.tool_calls) do local fn = tc["function"] or {} table.insert(inputMsgs, { type = "function_call", call_id = tostring(tc.id or ""), name = tostring(fn.name or ""), arguments = tostring(fn.arguments or "{}") }) end else table.insert(inputMsgs, item) end end return inputMsgs end local function shortReason(text) local s = tostring(text) if s:find("NEED_RESPONSES_API", 1, true) or s:find("/v1/responses", 1, true) then return "needs the Responses API" end if s:find("CONTENT_BLOCKED", 1, true) then return "content filter" end if s:find("NO_CHANNEL", 1, true) or s:find("无可用渠道", 1, true) then return "no channel in this key's group" end if s:find("HTTP 401", 1, true) or s:find("Authentication failed", 1, true) then return "key rejected" end if s:find("HTTP 402", 1, true) then return "premium quota empty" end if s:find("HTTP 429", 1, true) or s:find("Rate limited", 1, true) then return "rate limited" end if s:find("HTTP 404", 1, true) then return "model id not served" end return "failed" end local function arChatFallback(avoid, firstId) local candidates = fallbackModelList(avoid, firstId) local notes = {} for _, key in ipairs(arKeys()) do local arHeaders = { ["Content-Type"] = "application/json", ["Accept"] = "application/json", ["Authorization"] = "Bearer " .. key, } for _, fbId in ipairs(candidates) do local chatPayload = { model = fbId, messages = {{ role = "system", content = systemPrompt }}, stream = false, max_tokens = 4096, } for _, item in ipairs(messages) do local t = textOnly(item) if t then table.insert(chatPayload.messages, t) end end local okAr, outAr = pcall(function() return requestOpenAICompatible(API.AGENTROUTER_URL, chatPayload, arHeaders) end) if okAr then API.lastGoodModel = fbId return outAr, fbId, nil, "chat" end local text = tostring(outAr) if text:find("CONTENT_BLOCKED", 1, true) then API.blockedModels[fbId] = true end if text:find("NEED_RESPONSES_API", 1, true) or text:find("/v1/responses", 1, true) then local respPayload = { model = fbId, instructions = systemPrompt, input = responsesInput(), stream = false, max_output_tokens = 4096, } local okResp, outResp = pcall(function() return requestHttp(API.AGENTROUTER_RESPONSES_URL, "POST", arHeaders, HttpService:JSONEncode(respPayload)) end) if okResp then API.lastGoodModel = fbId return outResp, fbId, nil, "responses" end text = tostring(outResp) end notes[#notes + 1] = fbId .. ": " .. shortReason(text) if text:find("HTTP 401", 1, true) or text:find("Authentication failed", 1, true) then break end end end return nil, nil, table.concat(notes, " | "), nil end local function quotaFallback(err) local quota = isQuotaError(err) local noChannel = noChannelError(err) if not quota and not noChannel then return nil end if #arKeys() == 0 then return nil end local firstId = nil if noChannel then local okLive, live = pcall(function() return API.preferredModel(API.agentRouterModels(arKey())) end) if okLive then firstId = live end end local raw2, fbId, _, wire = arChatFallback(selectedModel, firstId) if not fbId then return nil end local okDecode, decoded2 = pcall(function() return decodeJson(raw2, "Fallback response") end) if not okDecode then return nil end local okParse, res2 = pcall(wire == "responses" and parseResponsesAnswer or parseChatAnswer, decoded2) if not okParse then return nil end res2.fellBack = fbId res2.fellBackReason = noChannel and "nochannel" or "quota" return res2 end -- DeepSeek filters the whole conversation, not just the last message, so a chat with a -- Roblox script in context is rejected before the prompt is even read. Rotate to another -- model instead of telling the user to switch by hand. local blockedNotes = nil local function blockedFallback(err) if not tostring(err):find("CONTENT_BLOCKED", 1, true) then return nil end if #arKeys() == 0 then return nil end API.blockedModels[selectedModel] = true local rawB, idB, notes, wireB = arChatFallback(selectedModel) blockedNotes = notes if not idB then return nil end local okDecode, decoded = pcall(function() return decodeJson(rawB, "Fallback response") end) if not okDecode then return nil end local okParse, res = pcall(wireB == "responses" and parseResponsesAnswer or parseChatAnswer, decoded) if not okParse then return nil end res.fellBack = idB res.fellBackReason = "blocked" return res end local function blockedMessage(extra) return "AgentRouter refused this turn as sensitive content on every model it could try. " .. "Pick another model in the menu, or remove the code from context and ask again." .. (extra and ("\n" .. extra) or "") .. (blockedNotes and ("\nTried: " .. blockedNotes) or "") end -- A model that already refused this conversation will refuse it again, so route straight -- to the one that answered instead of paying for the same rejection every turn. if not useOpenCode and API.blockedModels[selectedModel] and #arKeys() > 0 then local recovered = blockedFallback("CONTENT_BLOCKED|already refused") if recovered then if opts then opts._fellBack = recovered.fellBack opts._fellBackReason = "blocked" end return recovered end end if model.format == "anthropic" and not useOpenCode then local anthropicMessages, pending = {}, nil local function flushResults() if pending then table.insert(anthropicMessages, { role = "user", content = pending }) pending = nil end end for _, item in ipairs(messages) do if item.role == "tool" then pending = pending or {} table.insert(pending, { type = "tool_result", tool_use_id = tostring(item.tool_call_id or ""), content = tostring(item.content or "") }) else flushResults() if item.tool_calls then local blocks = {} if item.content ~= nil and tostring(item.content) ~= "" then table.insert(blocks, { type = "text", text = tostring(item.content) }) end for _, tc in ipairs(item.tool_calls) do local fn = tc["function"] or {} local input = {} if type(fn.arguments) == "string" and fn.arguments ~= "" then local okArgs, decArgs = pcall(function() return HttpService:JSONDecode(fn.arguments) end) if okArgs and type(decArgs) == "table" then input = decArgs end end table.insert(blocks, { type = "tool_use", id = tostring(tc.id or ""), name = tostring(fn.name or ""), input = input }) end table.insert(anthropicMessages, { role = "assistant", content = blocks }) else local content = item.content if type(content) == "table" then local converted = {} for _, part in ipairs(content) do if type(part) == "table" and part.type == "text" then table.insert(converted, { type = "text", text = part.text }) elseif type(part) == "table" and part.type == "image_url" and part.image_url and type(part.image_url.url) == "string" then local mediaType, encoded = part.image_url.url:match("^data:([^;]+);base64,(.+)$") if mediaType and encoded then table.insert(converted, { type = "image", source = { type = "base64", media_type = mediaType, data = encoded } }) end end end content = converted end table.insert(anthropicMessages, { role = item.role, content = content }) end end end flushResults() local payload = { model = selectedModel, system = systemPrompt, messages = anthropicMessages, max_tokens = 4096, } if hasTools then local defs = {} for _, t in ipairs(toolSchemas) do defs[#defs + 1] = { name = t.name, description = t.description, input_schema = schemaOf(t) } end payload.tools = defs end local okA, resA = pcall(function() return requestHttp(API.AGENTROUTER_MESSAGES_URL, "POST", { ["Content-Type"] = "application/json", ["Accept"] = "application/json", ["x-api-key"] = apiKey, ["Authorization"] = "Bearer " .. apiKey, ["anthropic-version"] = "2023-06-01", }, HttpService:JSONEncode(payload), API.CLAUDE_SPOOF) end) if not okA and isQuotaError(resA) then local fb = quotaFallback(resA) if fb then return fb end end if not okA then local fbBlocked = blockedFallback(resA) if fbBlocked then return fbBlocked end end if not okA then local okB, resB = pcall(function() return requestHttp(API.AGENTROUTER_MESSAGES_URL, "POST", { ["Content-Type"] = "application/json", ["Accept"] = "application/json", ["Authorization"] = "Bearer " .. apiKey, ["anthropic-version"] = "2023-06-01", }, HttpService:JSONEncode(payload), API.CLAUDE_SPOOF) end) if not okB then local fb = quotaFallback(resA) or quotaFallback(resB) if fb then return fb end -- Claude is also exposed over the OpenAI-compatible chat route on AgentRouter, -- so try that wire before switching models. local okChatClaude, resChatClaude = pcall(function() local chatPayload = { model = selectedModel, messages = {{ role = "system", content = systemPrompt }}, stream = false, max_tokens = 4096, } for _, item in ipairs(messages) do local t = textOnly(item) if t then table.insert(chatPayload.messages, t) end end if hasTools then chatPayload.tools = {} for _, t in ipairs(toolSchemas) do chatPayload.tools[#chatPayload.tools + 1] = { type = "function", ["function"] = { name = t.name, description = t.description, parameters = schemaOf(t) } } end chatPayload.tool_choice = "auto" end return requestOpenAICompatible(API.AGENTROUTER_URL, chatPayload, { ["Content-Type"] = "application/json", ["Accept"] = "application/json", ["Authorization"] = "Bearer " .. apiKey, }) end) if okChatClaude then local okChatDecode, chatDecoded = pcall(function() return decodeJson(resChatClaude, "Chat response") end) if okChatDecode then local okChatParse, chatResult = pcall(parseChatAnswer, chatDecoded) if okChatParse then return chatResult end end end error(API.describeError(resA)) end raw = resB else raw = resA end local decoded = decodeJson(raw, "Anthropic response") local parts = decoded.content or {} local answer, calls = {}, {} for _, part in ipairs(parts) do if type(part) == "table" then if part.type == "text" and part.text then table.insert(answer, part.text) elseif part.type == "tool_use" and part.name then table.insert(calls, { id = tostring(part.id or ("call_" .. tostring(#calls + 1))), name = tostring(part.name), args = type(part.input) == "table" and part.input or {} }) end end end if #answer == 0 and #calls == 0 then error(decoded.error and (decoded.error.message or decoded.error) or "No message content.") end return { text = table.concat(answer, "\n"), thinking = opts.think and "Reasoning mode was requested for this response." or nil, searches = {}, toolCalls = #calls > 0 and calls or nil } end local useResponses = (model.endpoint == "responses") and not useOpenCode and not useOpenRouter local chatUrl = useOpenRouter and API.OPENROUTER_URL or API.AGENTROUTER_URL local chatSpoof = useOpenRouter and API.OPENROUTER_SPOOF or nil local headers = { ["Content-Type"] = "application/json", ["Accept"] = "application/json", ["Authorization"] = "Bearer " .. apiKey, } local function responsesTools(into) if not hasTools then return end local defs = {} for _, t in ipairs(toolSchemas) do defs[#defs + 1] = { type = "function", name = t.name, description = t.description, parameters = schemaOf(t) } end into.tools = defs into.tool_choice = "auto" end local payload if useResponses then payload = { model = selectedModel, instructions = systemPrompt, input = responsesInput(), stream = false, max_output_tokens = 4096, } responsesTools(payload) if opts.think then payload.reasoning = { effort = "high", summary = "auto" } end else payload = { model = selectedModel, messages = {{ role = "system", content = systemPrompt }}, stream = false, max_tokens = 4096, } for _, item in ipairs(messages) do table.insert(payload.messages, item) end if opts.think then payload.reasoning_effort = "high" end local toolDefs = {} if useOpenCode then payload.stream = true payload.reasoning_effort = nil for _, t in ipairs(API.OPENCODE_TOOLS) do toolDefs[#toolDefs + 1] = t end end if hasTools then for _, t in ipairs(toolSchemas) do toolDefs[#toolDefs + 1] = { type = "function", ["function"] = { name = t.name, description = t.description, parameters = schemaOf(t), } } end end if #toolDefs > 0 then payload.tools = toolDefs payload.tool_choice = hasTools and "auto" or "none" end end local function isFreeTier(err) local s = tostring(err):lower() return s:find("opencode_free_tier", 1, true) ~= nil or s:find("freetier", 1, true) ~= nil or s:find("within opencode", 1, true) ~= nil or s:find("free usage limit", 1, true) ~= nil or s:find("freeusagelimit", 1, true) ~= nil end if useOpenCode then headers["Accept"] = "text/event-stream" headers["Authorization"] = nil -- the fingerprint spoof supplies the bearer local function opencodeAttempt(bearer) local ok1, err1 = pcall(function() raw = requestOpenAICompatible(API.OPENCODE_URL, payload, headers, opencodeSpoof(bearer)) end) if ok1 then return true end if isFreeTier(err1) then return false, err1 end local ok2, err2 = pcall(function() raw = requestOpenAICompatible(API.OPENCODE_INFERENCE_URL, payload, headers, opencodeSpoof(bearer)) end) if ok2 then return true end return false, err1, err2 end local okA, errA1, errA2 local stored = (apiKey ~= "" and apiKey ~= "public") and apiKey or "" -- Zen refuses its free models to external clients once the shared free pool is empty. -- Remember that for a while so every message does not wait on the same failure. if os.time() >= opencodeBlockedUntil then if stored ~= "" then okA, errA1, errA2 = opencodeAttempt(stored) end if not okA and not isFreeTier(errA1) then okA, errA1, errA2 = opencodeAttempt("public") end end if not okA and not isFreeTier(errA1) and os.time() >= opencodeBlockedUntil then -- A single saturated upstream worker does not mean the whole free pool is down, -- so walk the other free models before handing the turn to AgentRouter. for _, altId in ipairs({ "beam-free", "glm-5.3-flash-free", "mimo-v2.6-flash-free", "flint-40b-free", "nemotron-3.5-lightning-free", "big-pickle" }) do if altId ~= selectedModel then local altPayload = table.clone(payload) altPayload.model = altId local okAlt, errAlt = pcall(function() raw = requestOpenAICompatible(API.OPENCODE_URL, altPayload, headers, opencodeSpoof(stored ~= "" and stored or "public")) end) if okAlt then if opts then opts._fellBack = altId opts._fellBackReason = "zenbusy" end selectedModel = altId okA = true break end if isFreeTier(errAlt) then break end end end end if not okA then -- Any Zen failure is worth a fallback, not only the free-tier refusal: a 429, a -- retired endpoint or a bad stored Zen key all mean the same thing to the user. local freeTier = isFreeTier(errA1) or isFreeTier(errA2) local cooled = os.time() < opencodeBlockedUntil if freeTier then opencodeBlockedUntil = os.time() + 180 end if #arKeys() == 0 then if freeTier or cooled then error("OpenCode Zen's free pool is not serving requests right now, and no AgentRouter API key is set.\n" .. "Open Settings → Providers, paste a key in the AgentRouter field, press Save changes, then Retry. " .. "Free models then fall back to AgentRouter on their own.") end error(API.describeError(tostring(errA1 or errA2 or "OpenCode request failed."))) end local rawFb, idFb, notes, wireFb = arChatFallback(selectedModel) if not idFb then error(API.describeError(tostring(errA1 or errA2 or "OpenCode request failed.")) .. "\nAgentRouter fallback also failed." .. (notes and (" Tried: " .. notes) or "") .. "\nOpen Settings → Providers and pick another model, or retry in a minute.") end raw = rawFb if opts then opts._fellBack = idFb opts._fellBackReason = "opencode" end opencodeStream = false useResponses = wireFb == "responses" end elseif useResponses then local okR, errR = pcall(function() raw = requestHttp(API.AGENTROUTER_RESPONSES_URL, "POST", headers, HttpService:JSONEncode(payload)) end) if not okR then local errS = tostring(errR) local fbQuota = quotaFallback(errR) if fbQuota then return fbQuota end if errS:find("CONTENT_BLOCKED", 1, true) then local fbBlocked = blockedFallback(errS) if fbBlocked then return fbBlocked end error(blockedMessage()) end local okC, errC = pcall(function() local chatPayload = { model = selectedModel, messages = {{ role = "system", content = systemPrompt }}, stream = false, max_tokens = 4096, } for _, item in ipairs(messages) do local t = textOnly(item) if t then table.insert(chatPayload.messages, t) end end if opts.think then chatPayload.reasoning_effort = "high" end raw = requestOpenAICompatible(API.AGENTROUTER_URL, chatPayload, headers) useResponses = false end) if not okC then local fbQuota2 = quotaFallback(errC) if fbQuota2 then return fbQuota2 end if tostring(errC):find("CONTENT_BLOCKED", 1, true) then local fbBlocked2 = blockedFallback(errC) if fbBlocked2 then return fbBlocked2 end error(blockedMessage()) end error(API.describeError(errS)) end end else local okC, errC = pcall(function() raw = requestOpenAICompatible(chatUrl, payload, headers, chatSpoof) end) if not okC and payload.tools then -- Some models reject the tool schema outright. Retry the same turn without tools -- instead of failing the whole message. local plain = table.clone(payload) plain.tools = nil plain.tool_choice = nil local okPlain, errPlain = pcall(function() raw = requestOpenAICompatible(chatUrl, plain, headers, chatSpoof) end) if okPlain then okC, errC = true, nil else errC = errPlain end end if not okC then local errMsg = tostring(errC) -- OpenRouter's own errors (no credits, no endpoint, bad key) are actionable, so -- they are shown instead of being swapped for a reply from another provider. if useOpenRouter then error(API.describeError(errMsg)) end local fbQuota = quotaFallback(errMsg) if fbQuota then return fbQuota end if errMsg:find("CONTENT_BLOCKED", 1, true) then local fbBlocked3 = blockedFallback(errMsg) if fbBlocked3 then return fbBlocked3 end error(blockedMessage()) end if errMsg:find("NEED_RESPONSES_API", 1, true) or errMsg:lower():find("/v1/responses", 1, true) then local respPayload = { model = selectedModel, instructions = systemPrompt, input = responsesInput(), stream = false, max_output_tokens = 4096, } responsesTools(respPayload) if opts.think then respPayload.reasoning = { effort = "high", summary = "auto" } end local okResp, errResp = pcall(function() raw = requestHttp(API.AGENTROUTER_RESPONSES_URL, "POST", headers, HttpService:JSONEncode(respPayload)) end) if not okResp then local fbQuota2 = quotaFallback(errResp) if fbQuota2 then return fbQuota2 end error(API.describeError(tostring(errResp))) end useResponses = true else error(API.describeError(errMsg)) end end end local result if opencodeStream then result = parseOpencodeStream(raw) if not result then result = parseChatAnswer(decodeJson(raw, "Chat response")) end else local decoded = decodeJson(raw, "Chat response") if useResponses then result = parseResponsesAnswer(decoded) else result = parseChatAnswer(decoded) end end result.fellBack = opts and opts._fellBack or nil result.fellBackReason = opts and opts._fellBackReason or nil if not result.fellBack then API.lastGoodModel = selectedModel end return result end function runAgent(msg, history, model, opts) local TOOL_LIMITS = { read_console = 2, take_screenshot = 1, web_search = 4, web_fetch = 3, inspect_player = 2 } local agentMessages = nil webTainted = false local used = {} msg.taskPhase = "analysing" if msg._update then msg._update() end for step = 1, 4 do if step == 2 then msg.taskPhase = "planning" elseif step == 3 then msg.taskPhase = "editing" elseif step == 4 then msg.taskPhase = "finishing" end if msg._update then msg._update() end -- On the last pass force a normal answer so the turn cannot end on a tool call. local callOpts = opts if step == 4 and opts then callOpts = table.clone(opts) callOpts.tools = nil end if callOpts then callOpts._fellBack, callOpts._fellBackReason = nil, nil end local reply = getAIReply(history, model, callOpts, agentMessages) for _, s in ipairs(reply.searches or {}) do table.insert(msg.tools, { kind = "web", label = "Searched the web: " .. s, state = "done" }) end if reply.note then table.insert(msg.tools, { kind = "note", label = reply.note, state = "done" }) end msg.answeredById, msg.fellBackReason = reply.fellBack, reply.fellBackReason if reply.fellBack then local picked = model.short or model.name local got = modelById(reply.fellBack) local gotName = got and (got.short or got.name) or tostring(reply.fellBack) local why if reply.fellBackReason == "opencode" then why = picked .. " was unavailable" elseif reply.fellBackReason == "zenbusy" then why = picked .. " was busy upstream" elseif reply.fellBackReason == "nochannel" then why = picked .. " is not served by this key" elseif reply.fellBackReason == "blocked" then why = picked .. " rejected this as sensitive content" else why = "Premium quota was empty for " .. picked end table.insert(msg.tools, { kind = "note", label = why .. " · " .. gotName .. " answered instead", state = "done" }) end if msg._update then msg._update() end if not reply.toolCalls or #reply.toolCalls == 0 or step == 4 then msg.taskPhase = nil return reply end agentMessages = agentMessages or {} local rawCalls = {} for _, call in ipairs(reply.toolCalls) do local encodedArgs = "{}" if call.args and next(call.args) ~= nil then encodedArgs = HttpService:JSONEncode(call.args) end table.insert(rawCalls, { id = call.id, type = "function", ["function"] = { name = call.name, arguments = encodedArgs } }) end table.insert(agentMessages, { role = "assistant", content = reply.text or "", tool_calls = rawCalls }) for _, call in ipairs(reply.toolCalls) do if cancelFlag then return { text = "Stopped." } end local chip = { kind = call.name, label = "Using " .. tostring(call.name), state = "running" } table.insert(msg.tools, chip) if msg._update then msg._update() end local content, label, state, image if (used[call.name] or 0) >= (TOOL_LIMITS[call.name] or 1) then chip.label, chip.state = "Skipped (limit reached): " .. tostring(call.name), "denied" content = "Tool call limit reached for this turn." else used[call.name] = (used[call.name] or 0) + 1 content, label, state, image = runTool(call) chip.label, chip.state = label, state or "done" end table.insert(agentMessages, { role = "tool", tool_call_id = call.id, content = redactSecrets(content) }) if image and image.base64 then table.insert(agentMessages, { role = "user", content = { { type = "text", text = "Screenshot captured from the game:" }, { type = "image_url", image_url = { url = "data:" .. (image.mediaType or "image/png") .. ";base64," .. image.base64 } }, } }) end if msg._update then msg._update() end end end msg.taskPhase = nil return { text = "" } end function subUtf8(str, n, len) if n >= len then return str end return str:sub(1, utf8.offset(str, n + 1) - 1) end local function typeInto(msg, field, full, speed) local len = utf8.len(full) if not len then msg[field] = full return true end local i = 0 while i < len do if cancelFlag then return false end i = math.min(len, i + speed) msg[field] = subUtf8(full, i, len) if msg._update then msg._update() end scrollToBottom() task.wait(0.02) end return true end function generate() local convo = current local model = MODELS[modelIndex] local msg = { role = "assistant", text = "", model = model, status = "thinking", thinking = "", thinkOn = thinkOn, thinkOpen = thinkOn, thinkSecs = 0, tools = {}, startClock = os.clock(), taskPhase = "analysing", } table.insert(convo.messages, msg) busy, cancelFlag = true, false setLoading(true) renderMessages() updateComposer() task.spawn(function() while msg.status == "thinking" or msg.status == "planning" or msg.status == "reasoning" do msg.dots = ((msg.dots or 0) + 1) % 4 if msg._update then msg._update() end task.wait(0.35) end end) local started = os.clock() local history = { table.unpack(convo.messages, 1, #convo.messages - 1) } local opts = { think = msg.thinkOn, web = settings.web, tools = activeToolSchemas() } local ok, result = pcall(runAgent, msg, history, model, opts) if not ok then local errMsg = tostring(result):gsub("^.-:%d+:%s*", ""):gsub("CoreGui%.%S+%s*", "") if errMsg:find("NO_CHANNEL", 1, true) then errMsg = API.describeError(errMsg) elseif errMsg:find("OPENCODE_FREE_TIER", 1, true) then errMsg = "OpenCode refused the free-tier request (HTTP 403). The fingerprint may have changed — try another free model or switch to GPT-6.1 Sol." elseif errMsg:find("CONTENT_BLOCKED", 1, true) or errMsg:lower():find("content%-blocked") then errMsg = API.describeError(errMsg) elseif errMsg:find("402", 1, true) or errMsg:lower():find("batch quota", 1, true) then errMsg = "Claude/GPT quota is empty right now. AgentRouter releases it in daily batches (10:00 and 19:00 Beijing time); your account balance is not the limit. A free OpenCode Zen model still works meanwhile." end if #errMsg > 460 then errMsg = errMsg:sub(1, 460) .. "…" end msg.status, msg.text, msg.isError = "done", errMsg, true msg.taskPhase = nil else local reply = type(result) == "table" and result or { text = tostring(result) } msg.answeredById = reply.fellBack msg.fellBackReason = reply.fellBackReason local finished = true if msg.thinkOn and reply.thinking then msg.status = "reasoning" finished = typeInto(msg, "thinking", reply.thinking, 4) end msg.thinkSecs = math.max(1, math.floor(os.clock() - started + 0.5)) msg.thinkOpen = false msg.taskPhase = nil if finished then msg.status = "streaming" finished = typeInto(msg, "text", reply.text or "", 3) end msg.status = "done" if not finished and msg.text == "" then msg.text = "Stopped." end end busy = false setLoading(false) if convo == current then renderMessages() end updateComposer() end function send(override) if busy then cancelFlag = true return end local t = override or input.Text:match("^%s*(.-)%s*$") or "" if t == "" and #attachments == 0 then return end local m = MODELS[modelIndex] if #attachments > 0 and not (m.vision or m.visionId) then flashPlaceholder(m.name .. " can't read images. Switch models or remove them.") return end input.Text = "" if #current.messages == 0 then local title = t ~= "" and t or "Image chat" current.title = #title > 28 and (title:sub(1, 28) .. "...") or title refreshSidebar() end local images = nil if #attachments > 0 then images = attachments attachments = {} refreshAttachStrip() end table.insert(current.messages, { role = "user", text = t, images = images }) generate() end sendBtn.MouseButton1Click:Connect(function() send() end) newBtn.MouseButton1Click:Connect(function() newConversation(false) end) menuBtn.MouseButton1Click:Connect(function() setSidebar(not sidebarOpen) end) local function getTargetScale() local viewport = camera.ViewportSize local fitX = math.max(0.45, math.min(1, (viewport.X - 12) / 340)) local fitY = math.max(0.45, math.min(1, (viewport.Y - 12) / 280)) return math.min(settings.uiScale or 1, fitX, fitY) end local function setVisible(v) if v then window.Visible = true winScale.Scale = getTargetScale() * 0.94 tween(winScale, 0.18, { Scale = getTargetScale() }) else closeMenu() window.Visible = false end end closeBtn.MouseButton1Click:Connect(function() setVisible(false) end) minBtn.MouseButton1Click:Connect(function() setVisible(false) end) toggleBtn.MouseButton1Click:Connect(function() setVisible(not window.Visible) end) UserInputService.InputBegan:Connect(function(io, processed) if not processed and io.KeyCode == Enum.KeyCode.RightControl then setVisible(not window.Visible) end end) do local drag = { active = false, mouse = nil, pos = nil } header.InputBegan:Connect(function(io) if io.UserInputType == Enum.UserInputType.MouseButton1 or io.UserInputType == Enum.UserInputType.Touch then drag.active, drag.mouse, drag.pos = true, io.Position, window.Position end end) UserInputService.InputChanged:Connect(function(io) if drag.active and (io.UserInputType == Enum.UserInputType.MouseMovement or io.UserInputType == Enum.UserInputType.Touch) then local d = io.Position - drag.mouse window.Position = UDim2.new(drag.pos.X.Scale, drag.pos.X.Offset + d.X, drag.pos.Y.Scale, drag.pos.Y.Offset + d.Y) end end) UserInputService.InputEnded:Connect(function(io) if io.UserInputType == Enum.UserInputType.MouseButton1 or io.UserInputType == Enum.UserInputType.Touch then drag.active = false end end) end local function updateViewport() local viewport = camera.ViewportSize small = viewport.X < 700 SIDEBAR_W = small and math.max(150, math.floor(viewport.X * 0.82)) or 236 window.Size = UDim2.new(0.94, 0, 0.92, 0) winScale.Scale = getTargetScale() modelBtn.Size = UDim2.fromOffset(small and 116 or 140, 30) contextPct.Visible = not small titleLabel.Size = UDim2.new(1, small and -150 or -284, 0, 24) subtitleLabel.Size = UDim2.new(1, small and -150 or -284, 0, 16) if small then sidebar.ZIndex = 20 setSidebar(false, true) else sidebar.ZIndex = 1 setSidebar(sidebarOpen, true) end updateComposer() end camera:GetPropertyChangedSignal("ViewportSize"):Connect(updateViewport) setSidebar(sidebarOpen, true) applyTheme(settings.theme) updateViewport() -- Initialize Folders and Game Context -- projects = {} do local gameProj = newProject("[Game] " .. gameName) gameProj.context = "The player is currently in a Roblox game named '" .. gameName .. "'. PlaceId: " .. tostring(placeId) .. ". Use this context if the user asks for help related to the game." newProject("General Chats") currentProject = gameProj conversations = gameProj.conversations selectProject(gameProj) end newConversation(true) applyModel() updateComposer()