Skill Ai
Key Required

Skill Ai

Studio LiteDosrox_studioDosrox_studio 2.5K views 8K executesUpdated Jul 4, 2026
Unrated
00
Lua Script•3783 lines (169137 chars)
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local UIS = game:GetService("UserInputService")
local HttpService = game:GetService("HttpService")
local CoreGui = game:GetService("CoreGui")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Lighting = game:GetService("Lighting")
local StarterPack = game:GetService("StarterPack")
local ServerStorage = game:GetService("ServerStorage")
local StarterGui = game:GetService("StarterGui")
local StarterPlayer = game:GetService("StarterPlayer")

-- CRITICAL: StarterCharacterScripts and StarterPlayerScripts are special Roblox
-- objects with their own unique ClassName, NOT Folders with the same name. If we
-- Instance.new("Folder") and rename it "StarterPlayerScripts", it conflicts with
-- the real Roblox service object — creating a fake container that receives scripts
-- while the Explorer shows the real (empty) service. This was the root cause of
-- "Not Found" when clicking scripts, even though create_script returned "✅ Success".
-- Fix: use the existing real object (FindFirstChildOfClass), or if it doesn't exist
-- (rare, since these are default Roblox services), create with the correct ClassName.
local StarterCharacterScripts = StarterPlayer:FindFirstChildOfClass("StarterCharacterScripts")
if not StarterCharacterScripts then
    local ok, created = pcall(function() return Instance.new("StarterCharacterScripts") end)
    if ok and created then
        created.Parent = StarterPlayer
        StarterCharacterScripts = created
    end
end
local StarterPlayerScripts = StarterPlayer:FindFirstChildOfClass("StarterPlayerScripts")
if not StarterPlayerScripts then
    local ok, created = pcall(function() return Instance.new("StarterPlayerScripts") end)
    if ok and created then
        created.Parent = StarterPlayer
        StarterPlayerScripts = created
    end
end
local player = Players.LocalPlayer
math.randomseed(tick() * 10000)

local sceneState = {}
-- FIX v1.4: حد أقصى لـ sceneState — بعده نحذف أقدم مدخل لمنع تسرب الذاكرة
local SCENE_STATE_MAX = 60

local function pruneSceneState()
    local count = 0
    for _ in pairs(sceneState) do count = count + 1 end
    if count <= SCENE_STATE_MAX then return end
    -- احذف المدخلات الأقدم (lastAction الأصغر)
    local oldest_key, oldest_time = nil, math.huge
    for k, v in pairs(sceneState) do
        if (v.lastAction or 0) < oldest_time then
            oldest_key = k
            oldest_time = v.lastAction or 0
        end
    end
    if oldest_key then sceneState[oldest_key] = nil end
end

local function updateSceneState(action, didSucceed)
    if not didSucceed then return end
    if not action or type(action) ~= "table" then return end
    _searchCache = nil -- scene changed, invalidate search cache
    local containerKey = action.containerName or (action.name or action.className or "default")
    if not sceneState[containerKey] then
        sceneState[containerKey] = { className = action.className, count = 0, namePrefix = action.namePrefix, createdNames = {} }
    end
    local ss = sceneState[containerKey]
    ss.lastAction = os.time()
    ss.className = action.className or ss.className
    if action.namePrefix then ss.namePrefix = action.namePrefix end
    if action.type == "create_instance" then
        ss.count = ss.count + 1
        if action.name then ss.createdNames[action.name] = true end
    elseif action.type == "create_multiple" then
        local c = math.clamp(tonumber(action.count) or 1, 1, 200)
        ss.count = ss.count + c
    elseif action.type == "create_compound" then
        ss.count = ss.count + 1
        if action.name then ss.createdNames[action.name] = true end
    elseif action.type == "delete" then
        local targetName = action.target
        if targetName and ss.createdNames then
            for n, _ in pairs(ss.createdNames) do
                if n:lower():find(targetName:lower(), 1, true) then
                    ss.createdNames[n] = nil
                    ss.count = math.max(0, ss.count - 1)
                    break
                end
            end
        end
    end
    -- FIX v1.4: تنظيف تلقائي لمنع تسرب الذاكرة
    pruneSceneState()
end

local undoStack = {}
local undoLimit = 20
local _logLabels = {} -- cleared after each request, tracks addLog labels for cleanup
local _batchCreated = {} -- cleared before each batch
local _lastClickInstance = nil -- for clickable messages
local _batchPlacedGrid = nil -- tracks grid positions in create_multiple
local _autoRunScripts = {} -- scripts to re-execute on player respawn (max 20)
local AUTORUN_MAX = 20
local _requestQueue = {}
local _processingRequest = false
local _searchCache = nil  -- FIX: كان implicit global يُعاد تصفيره في updateSceneState بدون تعريف local
local MESSAGE_MAX = 50
local MAX_TOKENS_LOW = 16000
local MAX_TOKENS_HIGH = 32000

-- re-run registered scripts on every respawn so effects (crosshair, camera, GUI) persist
-- ═══════════════════════════════════════════════════════
-- MODEL REGISTRY — Free (OpenCode) + NVIDIA NIM
-- ═══════════════════════════════════════════════════════
local OPENCODE_URL  = "https://opencode.ai/zen/v1/chat/completions"
local NVIDIA_URL    = "https://integrate.api.nvidia.com/v1/chat/completions"

-- مفاتيح OpenCode (rotation)
local OPENCODE_KEYS = {
    "sk-ZP0Y71QDq4HhXrr5KgWEzsf8ZyNNsLJL070xXcGFUh8w7cvlFFJuks5k4jv7puCK",
    "sk-dtcKP4hx4iznBCaVm1uFvbEax12z22hlJzcW9alSIya8OQfSigpJN42QfxHuP1xk",
    "sk-Pe12GsYt4RnkTPtb7ACCGSj5vzz7u7m2INVDzxe1Z2ccGF904LFCsxpzouJek6ld",
    "sk-MN9kABCiW1cskBQ4EncGFV6DFwyHvshu6CrjtmuHUFwdkvJvAHNzql90BqTpNHMC",
    "sk-yXrnjOHCXVejD3v1Ngk2trqs8B2cFpQd9hbhBlpVyrNZgeevk3lPolVY0EtgQZha",
}
-- you need put it 
local NVIDIA_API_KEY = " "

-- 
local MODEL_LIST = {
    -- ── Free (OpenCode) ──────────────────────────────────
    { label = "⭐ Big Pickle (Free)",           provider = "opencode", id = "big-pickle" },
    { label = "DeepSeek V4 Flash (Free)",       provider = "opencode", id = "deepseek-v4-flash-free" },
    { label = "MiMo V2.5 (Free)",               provider = "opencode", id = "mimo-v2.5-free" },
    { label = "Nemotron 3 Ultra (Free)",        provider = "opencode", id = "nemotron-3-ultra-free" },
    { label = "North Mini Code (Free)",         provider = "opencode", id = "north-mini-code-free" },
    -- ── NVIDIA NIM (verified active 2026-06) ─────────────
    { label = "── NVIDIA NIM ──",               provider = "separator" },
    { label = "DeepSeek V4 Flash",              provider = "nvidia", id = "deepseek-ai/deepseek-v4-flash", maxOutput = 8192 },
    { label = "DeepSeek V4 Pro",                provider = "nvidia", id = "deepseek-ai/deepseek-v4-pro", maxOutput = 8192 },
    { label = "Llama 4 Maverick 17b",           provider = "nvidia", id = "meta/llama-4-maverick-17b-128e-instruct", maxOutput = 4096 },
    { label = "Llama 3.3 70b",                  provider = "nvidia", id = "meta/llama-3.3-70b-instruct", maxOutput = 4096 },
    { label = "Llama 3.1 70b",                  provider = "nvidia", id = "meta/llama-3.1-70b-instruct", maxOutput = 4096 },
    { label = "Llama 3.1 8b",                   provider = "nvidia", id = "meta/llama-3.1-8b-instruct", maxOutput = 4096 },
    { label = "Mistral Large 3 675b",           provider = "nvidia", id = "mistralai/mistral-large-3-675b-instruct-2512", maxOutput = 8192 },
    { label = "Mistral Medium 3.5 128b",        provider = "nvidia", id = "mistralai/mistral-medium-3.5-128b", maxOutput = 8192 },
    { label = "Mistral Small 4 119b",           provider = "nvidia", id = "mistralai/mistral-small-4-119b-2603", maxOutput = 8192 },
    { label = "Mistral 7b",                     provider = "nvidia", id = "mistralai/mistral-7b-instruct-v0.3", maxOutput = 4096 },
    { label = "Mixtral 8x22b",                  provider = "nvidia", id = "mistralai/mixtral-8x22b-v0.1", maxOutput = 4096 },
    { label = "Mixtral 8x7b",                   provider = "nvidia", id = "mistralai/mixtral-8x7b-instruct-v0.1", maxOutput = 4096 },
    { label = "Mistral Nemo 12b",               provider = "nvidia", id = "nv-mistralai/mistral-nemo-12b-instruct", maxOutput = 4096 },
    { label = "Qwen3.5 397b A17b",              provider = "nvidia", id = "qwen/qwen3.5-397b-a17b", maxOutput = 8192 },
    { label = "Qwen3.5 122b A10b",              provider = "nvidia", id = "qwen/qwen3.5-122b-a10b", maxOutput = 8192 },
    { label = "Qwen3-Next 80b A3b",             provider = "nvidia", id = "qwen/qwen3-next-80b-a3b-instruct", maxOutput = 8192 },
    { label = "Kimi K2.6 (1M ctx)",             provider = "nvidia", id = "moonshotai/kimi-k2.6", maxOutput = 8192 },
    { label = "GLM-5.1 (Zhipu)",                provider = "nvidia", id = "z-ai/glm-5.1", maxOutput = 8192 },
    { label = "Gemma 4 31b",                    provider = "nvidia", id = "google/gemma-4-31b-it", maxOutput = 4096 },
    { label = "Gemma 3 12b",                    provider = "nvidia", id = "google/gemma-3-12b-it", maxOutput = 4096 },
    { label = "Gemma 3n E4b",                   provider = "nvidia", id = "google/gemma-3n-e4b-it", maxOutput = 4096 },
    { label = "Phi-4 Mini",                     provider = "nvidia", id = "microsoft/phi-4-mini-instruct", maxOutput = 4096 },
    { label = "Phi-4 Multimodal",               provider = "nvidia", id = "microsoft/phi-4-multimodal-instruct", maxOutput = 4096 },
    { label = "Phi-3.5 MoE",                    provider = "nvidia", id = "microsoft/phi-3.5-moe-instruct", maxOutput = 4096 },
    { label = "Nemotron 3 Super 120b (1M ctx)", provider = "nvidia", id = "nvidia/nemotron-3-super-120b-a12b", maxOutput = 8192 },
    { label = "Nemotron 3 Ultra 253b",          provider = "nvidia", id = "nvidia/llama-3.1-nemotron-ultra-253b-v1", maxOutput = 8192 },
    { label = "Nemotron 3 Nano 30b",            provider = "nvidia", id = "nvidia/nemotron-3-nano-30b-a3b", maxOutput = 4096 },
    { label = "Nemotron 3 Nano Omni",           provider = "nvidia", id = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", maxOutput = 4096 },
    { label = "Llama 3.1 Nemotron 70b",         provider = "nvidia", id = "nvidia/llama-3.1-nemotron-70b-instruct", maxOutput = 4096 },
    { label = "Llama 3.3 Nemotron Super 49b",   provider = "nvidia", id = "nvidia/llama-3.3-nemotron-super-49b-v1", maxOutput = 4096 },
    { label = "MiniMax M2.7",                   provider = "nvidia", id = "minimaxai/minimax-m2.7", maxOutput = 8192 },
    { label = "MiniMax M3",                     provider = "nvidia", id = "minimaxai/minimax-m3", maxOutput = 8192 },
    { label = "Step 3.5 Flash",                 provider = "nvidia", id = "stepfun-ai/step-3.5-flash", maxOutput = 4096 },
    { label = "Step 3.7 Flash",                 provider = "nvidia", id = "stepfun-ai/step-3.7-flash", maxOutput = 4096 },
    { label = "GPT-OSS 120b",                   provider = "nvidia", id = "openai/gpt-oss-120b", maxOutput = 8192 },
    { label = "GPT-OSS 20b",                    provider = "nvidia", id = "openai/gpt-oss-20b", maxOutput = 4096 },
    { label = "Seed-OSS 36b (ByteDance)",       provider = "nvidia", id = "bytedance/seed-oss-36b-instruct", maxOutput = 8192 },
    { label = "DBRX Instruct (Databricks)",     provider = "nvidia", id = "databricks/dbrx-instruct", maxOutput = 4096 },
    { label = "Granite 34b Code (IBM)",         provider = "nvidia", id = "ibm/granite-34b-code-instruct", maxOutput = 4096 },
    { label = "Jamba 1.5 Large (AI21)",         provider = "nvidia", id = "ai21labs/jamba-1.5-large-instruct", maxOutput = 4096 },
    { label = "Sarvam M (multilingual)",        provider = "nvidia", id = "sarvamai/sarvam-m", maxOutput = 4096 },
    { label = "Llama Guard 4 12b",              provider = "nvidia", id = "meta/llama-guard-4-12b", maxOutput = 4096 },
}

-- الحالة الحالية — تُحمَّل من الملف عند البدء
local CURRENT_PROVIDER = "opencode"
local CURRENT_MODEL_LABEL = "⭐ Big Pickle (Free)"

-- ملف الحفظ
local SAVE_FILE = "SkillAI_ModelSave.txt"

local function saveModelChoice(label, provider, modelId)
    pcall(function()
        if writefile then
            local data = label .. "\n" .. provider .. "\n" .. (modelId or "")
            writefile(SAVE_FILE, data)
        end
    end)
end

local function loadModelChoice()
    local ok, data = pcall(function()
        if readfile and isfile and isfile(SAVE_FILE) then
            return readfile(SAVE_FILE)
        end
        return nil
    end)
    if ok and data and data ~= "" then
        local lines = {}
        for line in data:gmatch("[^\n]+") do table.insert(lines, line) end
        local savedLabel    = lines[1] or ""
        local savedProvider = lines[2] or "opencode"
        local savedId       = lines[3] or "big-pickle"
        -- ابحث عنه في القائمة
        for _, m in ipairs(MODEL_LIST) do
            if m.provider ~= "separator" and m.label == savedLabel then
                return m
            end
        end
        -- إذا مو موجود في القائمة، أنشئ entry مؤقت
        return { label = savedLabel, provider = savedProvider, id = savedId }
    end
    return nil
end

-- تطبيق اختيار النموذج (يعدّل API_URL + MODEL_ID + API_KEYS)
local API_URL   = OPENCODE_URL
local MODEL_ID  = "big-pickle"
local API_KEYS  = OPENCODE_KEYS
local KEY_INDEX = 0      -- FIX: تعريف واحد فقط هنا (كان مكرر بعدين كـ global implicit)
local KEY_RESET_TIME = 0 -- tick() when all keys exhausted

-- FIX v1.4: cache maxOutput عند تغيير النموذج بدل البحث في كل طلب
local CURRENT_MODEL_MAX_OUTPUT = nil

local function applyModelChoice(entry)
    if not entry or entry.provider == "separator" then return end
    CURRENT_PROVIDER     = entry.provider
    CURRENT_MODEL_LABEL  = entry.label
    MODEL_ID             = entry.id
    CURRENT_MODEL_MAX_OUTPUT = entry.maxOutput or nil  -- FIX v1.4
    if entry.provider == "nvidia" then
        API_URL  = NVIDIA_URL
        API_KEYS = { NVIDIA_API_KEY }
    else
        API_URL  = OPENCODE_URL
        API_KEYS = OPENCODE_KEYS
    end
    KEY_INDEX = 0
    saveModelChoice(entry.label, entry.provider, entry.id)
end

-- حمّل الاختيار المحفوظ عند البدء
do
    local saved = loadModelChoice()
    if saved then applyModelChoice(saved) end
end
local function getApiKey()
    if type(API_KEYS) ~= "table" or #API_KEYS == 0 then return nil end
    local key = API_KEYS[KEY_INDEX + 1] or API_KEYS[1]
    KEY_INDEX = (KEY_INDEX + 1) % #API_KEYS
    return key
end
local function currentKeyLabel()
    return "key " .. (KEY_INDEX % #API_KEYS + 1) .. "/" .. #API_KEYS
end

-- 💰 أسعار تقريبية للتوكنز (دولار لكل 1 مليون توكن)
local PRICE_PER_1M_INPUT = 0.15
local PRICE_PER_1M_OUTPUT = 0.60

local requestFunc = nil
if syn and syn.request then
    requestFunc = syn.request
elseif http_request then
    requestFunc = http_request
elseif request then
    requestFunc = request
elseif fluxus_request then
    requestFunc = fluxus_request
else
    requestFunc = function(data)
        return HttpService:PostAsync(data.Url, data.Body, Enum.HttpContentType.ApplicationJson, false, data.Headers)
    end
end

pcall(function()
    local old = CoreGui:FindFirstChild("Skill-AI")
    if old then old:Destroy() end
end)

local C = {
    bg = Color3.fromRGB(10, 10, 16),
    surface = Color3.fromRGB(18, 18, 26),
    surfaceHi = Color3.fromRGB(28, 28, 38),
    border = Color3.fromRGB(45, 45, 55),
    accent = Color3.fromRGB(0, 200, 255),
    text = Color3.fromRGB(240, 240, 245),
    textDim = Color3.fromRGB(140, 140, 155),
    green = Color3.fromRGB(0, 255, 150),
    red = Color3.fromRGB(255, 80, 80),
    yellow = Color3.fromRGB(255, 210, 0),
    orange = Color3.fromRGB(255, 140, 0),
}

local function corner(inst, r)
    local c = Instance.new("UICorner", inst)
    c.CornerRadius = UDim.new(0, r or 10)
    return c
end

local function stroke(inst, col, t, trans)
    local s = Instance.new("UIStroke", inst)
    s.Color = col or C.border
    s.Thickness = t or 1
    s.Transparency = trans or 0.2
    return s
end

local function tw(inst, info, props)
    local t = TweenService:Create(inst, info, props)
    t:Play()
    return t
end

local function makeDraggable(frame, handle)
    handle = handle or frame
    local dragging, dragStart, startPos = false, nil, nil
    handle.InputBegan:Connect(function(inp)
        if (inp.UserInputType == Enum.UserInputType.MouseButton1 or inp.UserInputType == Enum.UserInputType.Touch) then
            dragging = true
            dragStart = inp.Position
            startPos = frame.Position
        end
    end)
    UIS.InputChanged:Connect(function(inp)
        if dragging and (inp.UserInputType == Enum.UserInputType.MouseMovement or inp.UserInputType == Enum.UserInputType.Touch) then
            local d = inp.Position - dragStart
            frame.Position = UDim2.new(
                startPos.X.Scale, startPos.X.Offset + d.X,
                startPos.Y.Scale, startPos.Y.Offset + d.Y
            )
        end
    end)
    UIS.InputEnded:Connect(function(inp)
        if inp.UserInputType == Enum.UserInputType.MouseButton1 or inp.UserInputType == Enum.UserInputType.Touch then
            dragging = false
        end
    end)
end

local sg = Instance.new("ScreenGui")
sg.Name = "Skill-AI"
sg.ResetOnSpawn = false
sg.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
sg.IgnoreGuiInset = true
local sgParented = pcall(function() sg.Parent = CoreGui end)
if not sgParented then
    pcall(function() sg.Parent = Players.LocalPlayer:WaitForChild("PlayerGui") end)
end

local toggleBtn = Instance.new("TextButton")
toggleBtn.Name = "Toggle"
toggleBtn.Size = UDim2.new(0, 42, 0, 42)
toggleBtn.Position = UDim2.new(0.04, 0, 0.15, 0)
toggleBtn.BackgroundColor3 = C.surfaceHi
toggleBtn.Text = "🧠"
toggleBtn.TextColor3 = C.accent
toggleBtn.TextSize = 20
toggleBtn.Font = Enum.Font.GothamBold
toggleBtn.Parent = sg
corner(toggleBtn, 21)
stroke(toggleBtn, C.accent, 1.5, 0.3)
makeDraggable(toggleBtn)

local PW = 340
local PH = 440
local panel = Instance.new("Frame")
panel.Name = "Panel"
panel.Size = UDim2.new(0, PW, 0, PH)
panel.Position = UDim2.new(0.5, -PW/2, 0.5, -PH/2)
panel.BackgroundColor3 = C.bg
panel.BorderSizePixel = 0
panel.ClipsDescendants = true
panel.Parent = sg
corner(panel, 12)
stroke(panel, C.accent, 1, 0.7)

local tb = Instance.new("Frame")
tb.Size = UDim2.new(1, 0, 0, 34)
tb.BackgroundTransparency = 1
tb.Parent = panel

local titleText = Instance.new("TextLabel")
titleText.Size = UDim2.new(1, -130, 1, 0)
titleText.Position = UDim2.new(0, 12, 0, 0)
titleText.BackgroundTransparency = 1
titleText.Text = "🧠 Skill-AI v1.4"
titleText.TextColor3 = C.accent
titleText.TextXAlignment = Enum.TextXAlignment.Left
titleText.TextSize = 14
titleText.Font = Enum.Font.GothamBold
titleText.Parent = tb

-- زر اختيار النموذج
local modelBtn = Instance.new("TextButton")
modelBtn.Size = UDim2.new(0, 110, 0, 22)
modelBtn.Position = UDim2.new(1, -138, 0.5, -11)
modelBtn.BackgroundColor3 = C.surfaceHi
modelBtn.TextColor3 = C.yellow
modelBtn.TextSize = 9
modelBtn.Font = Enum.Font.Gotham
modelBtn.TextTruncate = Enum.TextTruncate.AtEnd
modelBtn.Parent = tb
corner(modelBtn, 5)
stroke(modelBtn, C.yellow, 1, 0.4)

local function updateModelBtn()
    local short = CURRENT_MODEL_LABEL:gsub("%(Free%)", ""):gsub("%(NVIDIA%)", ""):match("^%s*(.-)%s*$")
    if #short > 18 then short = short:sub(1, 17) .. "…" end
    modelBtn.Text = "⚙ " .. short
end
updateModelBtn()

-- ── Model Picker Popup ──────────────────────────────────────────────────────


local closeBtn = Instance.new("TextButton")
closeBtn.Size = UDim2.new(0, 22, 0, 22)
closeBtn.Position = UDim2.new(1, -28, 0.5, -11)
closeBtn.BackgroundColor3 = C.surfaceHi
closeBtn.Text = "✕"
closeBtn.TextColor3 = C.red
closeBtn.TextSize = 10
closeBtn.Font = Enum.Font.GothamBold
closeBtn.Parent = tb
corner(closeBtn, 6)
closeBtn.MouseButton1Click:Connect(function() sg:Destroy() end)

makeDraggable(panel, tb)

toggleBtn.MouseButton1Click:Connect(function()
    panel.Visible = not panel.Visible
    tw(toggleBtn, TweenInfo.new(0.3), {Rotation = panel.Visible and 0 or 180})
end)

local displayArea = Instance.new("ScrollingFrame")
displayArea.Size = UDim2.new(1, -16, 1, -115)
displayArea.Position = UDim2.new(0, 8, 0, 40)
displayArea.BackgroundColor3 = C.surface
displayArea.BorderSizePixel = 0
displayArea.ScrollBarThickness = 0
displayArea.AutomaticCanvasSize = Enum.AutomaticSize.Y
displayArea.CanvasSize = UDim2.new(0, 0, 0, 0)
displayArea.Parent = panel
corner(displayArea, 8)
stroke(displayArea, C.border, 1, 0.3)

local displayList = Instance.new("UIListLayout", displayArea)
displayList.Padding = UDim.new(0, 6)
displayList.HorizontalAlignment = Enum.HorizontalAlignment.Center
displayList.SortOrder = Enum.SortOrder.LayoutOrder

local welcomeLbl = Instance.new("TextLabel")
welcomeLbl.Size = UDim2.new(1, -10, 0, 40)
welcomeLbl.BackgroundTransparency = 1
welcomeLbl.Text = "💬 Ask me anything or give me a command — I plan and execute..."
welcomeLbl.TextColor3 = C.textDim
welcomeLbl.TextSize = 12
welcomeLbl.Font = Enum.Font.Gotham
welcomeLbl.Parent = displayArea

local inputArea = Instance.new("Frame")
inputArea.Size = UDim2.new(1, -16, 0, 52)
inputArea.Position = UDim2.new(0, 8, 1, -60)
inputArea.BackgroundColor3 = C.surface
inputArea.Parent = panel
corner(inputArea, 8)
stroke(inputArea, C.border, 1, 0.3)

local inputBox = Instance.new("TextBox")
inputBox.Size = UDim2.new(1, -64, 1, -8)
inputBox.Position = UDim2.new(0, 8, 0, 4)
inputBox.BackgroundTransparency = 1
inputBox.PlaceholderText = "Type any command in any language..."
inputBox.TextColor3 = C.text
inputBox.PlaceholderColor3 = C.textDim
inputBox.TextSize = 13
inputBox.Font = Enum.Font.Gotham
inputBox.ClearTextOnFocus = false
inputBox.Parent = inputArea

local sendBtn = Instance.new("TextButton")
sendBtn.Size = UDim2.new(0, 44, 1, -8)
sendBtn.Position = UDim2.new(1, -52, 0, 4)
sendBtn.BackgroundColor3 = C.accent
sendBtn.Text = "➤"
sendBtn.TextColor3 = Color3.fromRGB(0, 0, 0)
sendBtn.TextSize = 18
sendBtn.Font = Enum.Font.GothamBold
sendBtn.Parent = inputArea
corner(sendBtn, 6)

local function getInstancePath(inst)
    if not inst then return "nil" end
    local parts = {}
    local current = inst
    while current do
        local name = current.Name or "?"
        table.insert(parts, 1, name)
        current = current.Parent
    end
    return table.concat(parts, " > ")
end

-- Popup window showing full generated script code with a real copy button.
-- Used when direct Source writing fails (executor blocks it): shows the code
-- ready to copy/paste instead of trying to bypass the restriction.
local function showScriptPopup(code, scriptName)
    local overlay = Instance.new("Frame")
    overlay.Size = UDim2.new(1, 0, 1, 0)
    overlay.BackgroundColor3 = Color3.new(0, 0, 0)
    overlay.BackgroundTransparency = 0.4
    overlay.ZIndex = 50
    overlay.Parent = sg

    local box = Instance.new("Frame")
    box.Size = UDim2.new(0.9, 0, 0.7, 0)
    box.Position = UDim2.new(0.05, 0, 0.15, 0)
    box.BackgroundColor3 = C.surface
    box.ZIndex = 51
    box.Parent = overlay
    corner(box, 12)
    stroke(box, C.accent, 1.5, 0.2)

    local title = Instance.new("TextLabel")
    title.Size = UDim2.new(1, -16, 0, 36)
    title.Position = UDim2.new(0, 8, 0, 4)
    title.BackgroundTransparency = 1
    title.Text = "📝 " .. (scriptName or "Script") .. " — Copy this code into a new Script"
    title.TextColor3 = C.accent
    title.TextSize = 13
    title.Font = Enum.Font.GothamBold
    title.TextWrapped = true
    title.ZIndex = 52
    title.Parent = box

    local codeScroll = Instance.new("ScrollingFrame")
    codeScroll.Size = UDim2.new(1, -16, 1, -90)
    codeScroll.Position = UDim2.new(0, 8, 0, 40)
    codeScroll.BackgroundColor3 = C.bg
    codeScroll.ScrollBarThickness = 6
    codeScroll.ZIndex = 52
    codeScroll.Parent = box
    corner(codeScroll, 8)

    local codeLbl = Instance.new("TextLabel")
    codeLbl.Size = UDim2.new(1, -12, 0, 0)
    codeLbl.Position = UDim2.new(0, 6, 0, 6)
    codeLbl.BackgroundTransparency = 1
    codeLbl.Text = code
    codeLbl.TextColor3 = C.green
    codeLbl.TextSize = 12
    codeLbl.Font = Enum.Font.Code
    codeLbl.TextWrapped = true
    codeLbl.TextXAlignment = Enum.TextXAlignment.Left
    codeLbl.TextYAlignment = Enum.TextYAlignment.Top
    codeLbl.ZIndex = 53
    codeLbl.Parent = codeScroll

    local lineCount = select(2, code:gsub("\n", "\n")) + 1
    local approxHeight = math.max(100, lineCount * 16 + (#code / 50) * 14)
    codeLbl.Size = UDim2.new(1, -12, 0, approxHeight)
    codeScroll.CanvasSize = UDim2.new(0, 0, 0, approxHeight + 12)

    local copyBtn = Instance.new("TextButton")
    copyBtn.Size = UDim2.new(0.48, -4, 0, 36)
    copyBtn.Position = UDim2.new(0, 8, 1, -42)
    copyBtn.BackgroundColor3 = C.accent
    copyBtn.Text = "📋 Copy Code"
    copyBtn.TextColor3 = Color3.new(0, 0, 0)
    copyBtn.TextSize = 13
    copyBtn.Font = Enum.Font.GothamBold
    copyBtn.ZIndex = 52
    copyBtn.Parent = box
    corner(copyBtn, 8)

    copyBtn.MouseButton1Click:Connect(function()
        local copied = false
        if setclipboard then
            copied = pcall(function() setclipboard(code) end)
        end
        if copied then
            copyBtn.Text = "✅ Copied! Paste into a Script in the Explorer"
        else
            copyBtn.Text = "❌ Clipboard not supported by this Executor"
        end
    end)

    local closeBtn2 = Instance.new("TextButton")
    closeBtn2.Size = UDim2.new(0.48, -4, 0, 36)
    closeBtn2.Position = UDim2.new(0.52, 0, 1, -42)
    closeBtn2.BackgroundColor3 = C.surfaceHi
    closeBtn2.Text = "Close"
    closeBtn2.TextColor3 = C.text
    closeBtn2.TextSize = 13
    closeBtn2.Font = Enum.Font.Gotham
    closeBtn2.ZIndex = 52
    closeBtn2.Parent = box
    corner(closeBtn2, 8)
    closeBtn2.MouseButton1Click:Connect(function()
        overlay:Destroy()
    end)
end

local showInstanceInfo

local function addMessage(text, isUser, isError, isSystem, clickInstance)
    welcomeLbl.Visible = false

    -- FIX v1.4: message cap أسرع — بدل GetChildren() في كل message نتتبع العداد
    local allEntries = displayArea:GetChildren()
    local entryCount = #allEntries
    if entryCount > MESSAGE_MAX then
        local toRemove = entryCount - MESSAGE_MAX
        for i = 1, toRemove do
            local oldest = allEntries[i]
            if oldest and oldest ~= welcomeLbl and oldest ~= displayList then
                oldest:Destroy()
            end
        end
    end

    local entry = Instance.new("Frame")
    entry.Size = UDim2.new(1, -10, 0, 0)
    entry.BackgroundTransparency = 1
    entry.Parent = displayArea

    local bg = Instance.new("Frame")
    bg.Size = UDim2.new(1, 0, 0, 0)
    if isError then
        bg.BackgroundColor3 = C.red
    elseif isSystem then
        bg.BackgroundColor3 = Color3.fromRGB(30, 30, 20)
    elseif isUser then
        bg.BackgroundColor3 = C.surfaceHi
    else
        bg.BackgroundColor3 = C.surface
    end
    bg.Parent = entry
    corner(bg, 8)
    stroke(bg, isError and C.red or (isUser and C.accent or C.border), 0.5, 0.3)

    local lbl = Instance.new("TextButton")
    lbl.Size = UDim2.new(1, -16, 1, -8)
    lbl.Position = UDim2.new(0, 8, 0, 4)
    lbl.BackgroundTransparency = 1
    lbl.Text = text
    lbl.TextColor3 = isError and C.text or (isUser and C.accent or C.text)
    lbl.TextSize = 12
    lbl.Font = Enum.Font.Gotham
    lbl.TextWrapped = true
    lbl.TextXAlignment = Enum.TextXAlignment.Left
    lbl.AutoButtonColor = false
    lbl.Selectable = false
    lbl.Parent = bg

    if clickInstance then
        local actual = clickInstance
        if type(actual) == "table" and actual._ref then
            actual = actual._ref
        end
        if actual and type(actual) == "userdata" then
            lbl.TextColor3 = Color3.fromRGB(100, 180, 255)
            lbl.MouseButton1Click:Connect(function()
                showInstanceInfo(actual)
            end)
            entry.BackgroundTransparency = 0.85
        end
    end

    -- FIX v1.4: حساب الارتفاع الدقيق بدون مسح النص — O(1) بدل O(n)
    -- نعتمد على عدد السطور الفعلي + طول النص كمؤشر للف
    local lineBreaks = select(2, text:gsub("\n", "\n"))
    local wrapLines = math.floor(#text / 42) -- تقدير الف عند عرض ~42 حرف
    local totalLines = math.max(1, lineBreaks + wrapLines)
    local height = math.max(30, math.min(300, totalLines * 16 + 18))
    bg.Size = UDim2.new(1, 0, 0, height)
    entry.Size = UDim2.new(1, 0, 0, height + 4)

    task.wait()
    displayArea.CanvasPosition = Vector2.new(0, displayArea.CanvasSize.Y.Offset)
end

showInstanceInfo = function(inst)
    if not inst then
        addMessage("❌ Instance no longer exists", false, true)
        return
    end
    local className = inst:IsA("BasePart") and inst.ClassName or inst.ClassName or "Instance"
    local info = "📦 " .. className
    info = info .. " \"" .. inst.Name .. "\"\n📁 " .. getInstancePath(inst)
    if inst:IsA("BasePart") then
        local pos = inst.Position
        info = info .. ("\n📍 %.1f, %.1f, %.1f"):format(pos.X, pos.Y, pos.Z)
    end
    if inst:IsA("Script") or inst:IsA("LocalScript") then
        local src = inst.Source or ""
        if #src > 0 then
            info = info .. "\n📝 " .. #src .. " chars"
        else
            info = info .. "\n⚠️ Source is empty."
        end
    end
    if inst:IsA("StringValue") then
        local val = inst.Value or ""
        if #val > 0 then
            info = info .. "\n━━━━━━━━━━━━━━━━━━\n" .. val .. "\n━━━━━━━━━━━━━━━━━━"
        else
            info = info .. "\n⚠️ StringValue is empty"
        end
    end
    addMessage(info, false, false, true)
end

-- Model picker popup
local function showModelPicker()
    -- overlay
    local overlay = Instance.new("Frame")
    overlay.Size = UDim2.new(1, 0, 1, 0)
    overlay.BackgroundColor3 = Color3.new(0, 0, 0)
    overlay.BackgroundTransparency = 0.5
    overlay.ZIndex = 60
    overlay.Parent = sg

    local popup = Instance.new("Frame")
    popup.Size = UDim2.new(0, 290, 0, 380)
    popup.Position = UDim2.new(0.5, -145, 0.5, -190)
    popup.BackgroundColor3 = C.bg
    popup.ZIndex = 61
    popup.Parent = overlay
    corner(popup, 12)
    stroke(popup, C.accent, 1.5, 0.2)

    local hdr = Instance.new("TextLabel")
    hdr.Size = UDim2.new(1, -40, 0, 32)
    hdr.Position = UDim2.new(0, 10, 0, 4)
    hdr.BackgroundTransparency = 1
    hdr.Text = "⚙ Select Model"
    hdr.TextColor3 = C.accent
    hdr.TextXAlignment = Enum.TextXAlignment.Left
    hdr.TextSize = 13
    hdr.Font = Enum.Font.GothamBold
    hdr.ZIndex = 62
    hdr.Parent = popup

    local closePop = Instance.new("TextButton")
    closePop.Size = UDim2.new(0, 24, 0, 24)
    closePop.Position = UDim2.new(1, -30, 0, 4)
    closePop.BackgroundColor3 = C.surfaceHi
    closePop.Text = "✕"
    closePop.TextColor3 = C.red
    closePop.TextSize = 10
    closePop.Font = Enum.Font.GothamBold
    closePop.ZIndex = 62
    closePop.Parent = popup
    corner(closePop, 6)
    closePop.MouseButton1Click:Connect(function() overlay:Destroy() end)

    -- Current model label
    local curLbl = Instance.new("TextLabel")
    curLbl.Size = UDim2.new(1, -12, 0, 20)
    curLbl.Position = UDim2.new(0, 6, 0, 36)
    curLbl.BackgroundTransparency = 1
    curLbl.Text = "Now: " .. CURRENT_MODEL_LABEL
    curLbl.TextColor3 = C.green
    curLbl.TextSize = 10
    curLbl.Font = Enum.Font.Gotham
    curLbl.TextTruncate = Enum.TextTruncate.AtEnd
    curLbl.TextXAlignment = Enum.TextXAlignment.Left
    curLbl.ZIndex = 62
    curLbl.Parent = popup

    local scroll = Instance.new("ScrollingFrame")
    scroll.Size = UDim2.new(1, -12, 1, -64)
    scroll.Position = UDim2.new(0, 6, 0, 58)
    scroll.BackgroundColor3 = C.surface
    scroll.ScrollBarThickness = 4
    scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
    scroll.CanvasSize = UDim2.new(0, 0, 0, 0)
    scroll.ZIndex = 62
    scroll.Parent = popup
    corner(scroll, 8)

    local list = Instance.new("UIListLayout", scroll)
    list.Padding = UDim.new(0, 3)
    list.SortOrder = Enum.SortOrder.LayoutOrder

    local pad = Instance.new("UIPadding", scroll)
    pad.PaddingLeft = UDim.new(0, 4)
    pad.PaddingTop  = UDim.new(0, 4)

    for idx, entry in ipairs(MODEL_LIST) do
        if entry.provider == "separator" then
            local sep = Instance.new("TextLabel")
            sep.Size = UDim2.new(1, -8, 0, 20)
            sep.BackgroundColor3 = C.surfaceHi
            sep.Text = "── NVIDIA NIM ──"
            sep.TextColor3 = C.yellow
            sep.TextSize = 10
            sep.Font = Enum.Font.GothamBold
            sep.LayoutOrder = idx
            sep.ZIndex = 63
            sep.Parent = scroll
            corner(sep, 4)
        else
            local isActive = (entry.label == CURRENT_MODEL_LABEL)
            local btn = Instance.new("TextButton")
            btn.Size = UDim2.new(1, -8, 0, 26)
            btn.BackgroundColor3 = isActive and C.accent or C.surfaceHi
            btn.TextColor3 = isActive and Color3.new(0,0,0) or C.text
            btn.Text = entry.label
            btn.TextSize = 10
            btn.Font = Enum.Font.Gotham
            btn.TextXAlignment = Enum.TextXAlignment.Left
            btn.TextTruncate = Enum.TextTruncate.AtEnd
            btn.LayoutOrder = idx
            btn.ZIndex = 63
            btn.Parent = scroll
            corner(btn, 5)

            local lbl2 = Instance.new("UIPadding", btn)
            lbl2.PaddingLeft = UDim.new(0, 6)

            local capturedEntry = entry
            btn.MouseButton1Click:Connect(function()
                applyModelChoice(capturedEntry)
                updateModelBtn()
                curLbl.Text = "✅ Now: " .. CURRENT_MODEL_LABEL
                for _, child in ipairs(scroll:GetChildren()) do
                    if child:IsA("TextButton") then
                        child.BackgroundColor3 = C.surfaceHi
                        child.TextColor3 = C.text
                    end
                end
                btn.BackgroundColor3 = C.accent
                btn.TextColor3 = Color3.new(0, 0, 0)
                task.delay(0.2, function()
                    overlay:Destroy()
                    addMessage("✅ Model changed → **" .. CURRENT_MODEL_LABEL .. "**\nProvider: " .. CURRENT_PROVIDER:upper() .. " | Saved automatically.", false)
                end)
            end)
        end
    end
end

modelBtn.MouseButton1Click:Connect(showModelPicker)

local LOG_FILE = "SkillAI_Log.txt"

local function logToFile(label, content)
    local ok = pcall(function()
        local stamp = "\n[" .. os.date("%X") .. "] " .. label .. ":\n" .. tostring(content) .. "\n" .. string.rep("-", 30) .. "\n"
        if writefile then
            if isfile and isfile(LOG_FILE) and readfile then
                local existing = readfile(LOG_FILE)
                writefile(LOG_FILE, existing .. stamp)
            else
                writefile(LOG_FILE, stamp)
            end
        end
    end)
    return ok
end

local function setupRespawnReRunner()
    local plr = Players.LocalPlayer
    local function reRunAll()
        for _, entry in ipairs(_autoRunScripts) do
            local fn, err = loadstring("local script = ...\n" .. entry.source)
            if fn then
                task.spawn(function()
                    local ok, e = pcall(fn, entry.inst)
                    if not ok then logToFile("Respawn re-run error " .. entry.name, e) end
                end)
            end
        end
    end
    reRunAll()
    plr.CharacterAdded:Connect(reRunAll)
end

-- 🧠 تصنيف ذكي لتعقيد الطلب — يصير محلياً بالكود (فوري، صفر توكنز، صفر وقت
-- إضافي)، يقرر هل الطلب يحتاج تفكير عميق (high) أو بسيط (low). هذا يحل
-- التناقض "أريد دقة high بسرعة low": الحل مو إعداد ثابت واحد، بل نستخدم
-- high فقط للطلبات اللي فعلاً تحتاجه (سكربتات، أنظمة، خطط معقدة)، وlow
-- لكل شي آخر (يغطي أغلب الطلبات اليومية: قطعة، أداة، تعديل، حذف، إلخ).
local HIGH_EFFORT_KEYWORDS = {
    "script", "system", "unlock", "lock", "track", "when ", "challenge",
    "complete", "trigger", "damage", "spawn enemies", "ai", "npc",
    "leaderboard", "datastore", "save", "load", "remote", "multiplayer",
    "quest", "checkpoint", "gate", "obstacle course", "lap", "stage",
    "wave", "phase", "progression", "rank", "level", "door", "key",
    "boss", "health bar", "timer", "countdown", "score", "points",
    "سكربت", "نظام", "يفتح", "يقفل", "تحدي", "يتتبع",
    "بوابة", "مرحلة", "مستوى", "نقاط", "مؤقت", "بوس",
}
local function classifyEffort(question)
    if type(question) ~= "string" then return "low" end
    local lowerQ = question:lower()
    -- طول الطلب نفسه مؤشر قوي: طلبات طويلة ومفصلة غالباً معقدة منطقياً
    if #question > 220 then return "high" end
    for _, kw in ipairs(HIGH_EFFORT_KEYWORDS) do
        if lowerQ:find(kw, 1, true) then
            return "high"
        end
    end
    return "low"
end

-- FIX v1.4: مهلة الطلب — يمنع التجميد اللانهائي عند انقطاع الشبكة
local REQUEST_TIMEOUT = math.huge

local function callAI(messages, _isRetry, effort, _keyAttempt)
    _keyAttempt = _keyAttempt or 0
    local maxKeyAttempts = (#API_KEYS or 1) * 2

    local currentKey = getApiKey()
    if not currentKey then
        return nil, "❌ No API keys configured. Add keys to API_KEYS table."
    end

    -- FIX v1.4: cache modelMaxOutput بدل البحث في كل مرة
    local cap = (effort == "high") and MAX_TOKENS_HIGH or MAX_TOKENS_LOW
    local maxTokens = math.min(cap, CURRENT_MODEL_MAX_OUTPUT or cap)
    local data = {
        model = MODEL_ID,
        messages = messages,
        max_tokens = maxTokens,
        temperature = 0.2,
        top_p = 0.95,
    }

    local json = HttpService:JSONEncode(data)
    local headers = {
        ["Content-Type"] = "application/json",
        ["Authorization"] = "Bearer " .. currentKey,
    }

    -- FIX v1.4: Timeout wrapper — الطلب يتوقف بعد REQUEST_TIMEOUT ثانية
    local success, response
    do
        local done = false
        local result_ok, result_val
        task.spawn(function()
            result_ok, result_val = pcall(function()
                if requestFunc then
                    return requestFunc({
                        Url = API_URL,
                        Method = "POST",
                        Headers = headers,
                        Body = json,
                    })
                else
                    return HttpService:PostAsync(API_URL, json, Enum.HttpContentType.ApplicationJson, false, headers)
                end
            end)
            done = true
        end)
        local t0 = tick()
        while not done and (tick() - t0) < REQUEST_TIMEOUT do
            task.wait(0.2)
        end
        if not done then
            return nil, "❌ Request timed out after " .. REQUEST_TIMEOUT .. "s. Check connection."
        end
        success, response = result_ok, result_val
    end

    if not success then
        if not _isRetry then
            -- FIX v1.4: 0.8s بدل 2s على الـ retry الأول — أسرع
            task.wait(0.8)
            return callAI(messages, true, effort, _keyAttempt)
        end
        return nil, "❌ Connection failed: " .. tostring(response)
    end

    local body = response.Body or response
    local okDecode, decoded = pcall(function() return HttpService:JSONDecode(body) end)
    if not okDecode or not decoded then
        logToFile("JSON decode failed", tostring(body):sub(1, 500))
        if not _isRetry then
            task.wait(0.8) -- FIX v1.4: أسرع
            return callAI(messages, true, effort, _keyAttempt)
        end
        return nil, "❌ Server returned invalid JSON. Check " .. LOG_FILE
    end

    local errMsg = nil
    if decoded.error then
        if type(decoded.error) == "table" then
            errMsg = tostring(decoded.error.message or decoded.error.code or "")
        elseif type(decoded.error) == "string" then
            errMsg = decoded.error
        end
    elseif decoded.message then
        errMsg = tostring(decoded.message)
    elseif decoded.detail then
        errMsg = tostring(decoded.detail)
    end
    if errMsg then
        logToFile("API error", errMsg)
        if errMsg:find("FreeUsageLimitError", 1, true) or errMsg:find("rate limit", 1, true) or errMsg:find("quota", 1, true) then
            if _keyAttempt < maxKeyAttempts then
                local backoff = math.min(2 ^ (_keyAttempt - math.floor(_keyAttempt / #API_KEYS)), 60)
                task.wait(backoff)
                return callAI(messages, _isRetry, effort, _keyAttempt + 1)
            end
            KEY_RESET_TIME = tick()
            return nil, "❌ All API keys exhausted. Wait ~60s or add new keys. Last key: " .. currentKeyLabel()
        end
        return nil, "❌ API: " .. errMsg
    end

    if decoded.choices and decoded.choices[1] then
        local msg = decoded.choices[1].message
        local content = msg and msg.content
        local usage = decoded.usage
        if content and content ~= "" then
            return content, nil, usage
        end
        local reasoning = msg and msg.reasoning_content
        if reasoning and reasoning ~= "" then
            return reasoning, nil, usage
        end
        logToFile("Model returned no content", pcall(function() return HttpService:JSONEncode(decoded) end) and HttpService:JSONEncode(decoded) or tostring(decoded))
        return nil, "⚠️ Model did not generate a response. Try again."
    end

    local decodedStr = pcall(function() return HttpService:JSONEncode(decoded) end) and HttpService:JSONEncode(decoded) or tostring(decoded)
    logToFile("No choices in response", decodedStr)
    if not _isRetry then
        task.wait(0.8) -- FIX v1.4: أسرع
        return callAI(messages, true, effort, _keyAttempt)
    end
    return nil, "❌ Unexpected server response. Check " .. LOG_FILE
end

local PARENT_MAP = {
    Workspace = Workspace,
    StarterPack = StarterPack,
    Lighting = Lighting,
    ServerStorage = ServerStorage,
    ReplicatedStorage = ReplicatedStorage,
    StarterGui = StarterGui,
    StarterPlayer = StarterPlayer,
    StarterCharacterScripts = StarterCharacterScripts,
    StarterPlayerScripts = StarterPlayerScripts,
    CoreGui = CoreGui,
    Players = Players,
}

local function resolveParent(name, fallback)
    if type(name) == "string" and PARENT_MAP[name] then
        return PARENT_MAP[name]
    end
    return fallback or Workspace
end

local function fixLuaCode(source, compileErr)
    local sysContent = "You fix Lua syntax errors. Return ONLY the corrected Lua code. No explanations, no markdown."
    local userContent = "Fix this Lua error: " .. compileErr .. "\n\n```lua\n" .. source .. "\n```"
    local fixMessages
    if CURRENT_PROVIDER == "nvidia" then
        fixMessages = { { role = "user", content = sysContent .. "\n\n" .. userContent } }
    else
        fixMessages = {
            { role = "system", content = sysContent },
            { role = "user", content = userContent }
        }
    end
    local fixBody = HttpService:JSONEncode({
        model = MODEL_ID,
        messages = fixMessages,
        max_tokens = 1000,
        temperature = 0.1
    })
    local fixKey = getApiKey() or ""
    local ok, response = pcall(function()
        return requestFunc({
            Url = API_URL,
            Method = "POST",
            Headers = {
                ["Content-Type"] = "application/json",
                ["Authorization"] = "Bearer " .. fixKey
            },
            Body = fixBody
        })
    end)
    if not ok or not response then return nil end
    local ok2, data = pcall(HttpService.JSONDecode, HttpService, response.Body)
    if not ok2 or not data or not data.choices or not data.choices[1] then return nil end
    local fixed = data.choices[1].message.content or ""
    fixed = fixed:gsub("```lua", ""):gsub("```", ""):gsub("^%s+", ""):gsub("%s+$", "")
    local fn, err2 = loadstring("local script = ...\n" .. fixed)
    if fn then return fixed end
    return nil, err2
end

local MAX_HISTORY_TURNS = 8
local conversationHistory = {} -- each entry: { user = "...", summary = "..." }
local CURRENT_REQUEST_TEXT = "" -- stores the latest user request text for context safety checks (e.g., fog detection)

local function pushHistory(userMsg, summary)
    table.insert(conversationHistory, { user = userMsg, summary = summary })
    while #conversationHistory > MAX_HISTORY_TURNS do
        table.remove(conversationHistory, 1)
    end
end

local function buildHistoryMessages()
    local msgs = {}
    -- Only send last exchange (saves tokens, speeds up response)
    local startIdx = math.max(1, #conversationHistory - 1)
    for i = startIdx, #conversationHistory do
        local turn = conversationHistory[i]
        table.insert(msgs, { role = "user", content = turn.user })
        table.insert(msgs, { role = "assistant", content = turn.summary })
    end
    return msgs
end

local SEARCH_SERVICES = { Workspace, StarterPack, Lighting, ServerStorage, ReplicatedStorage, StarterGui, StarterPlayer, StarterCharacterScripts, StarterPlayerScripts, Players, CoreGui }
-- _searchCache معرَّف في الأعلى (سطر ~88) — لا تعريف مكرر هنا
local _searchCacheTime = 0
-- FIX v1.4: رفعنا TTL من 0.3 → 1.5 ثانية — المشهد نادراً يتغير بين طلبين متتاليين
local SEARCH_CACHE_TTL = 1.5
local function searchAllDescendants()
    if _searchCache and tick() - _searchCacheTime < SEARCH_CACHE_TTL then
        return _searchCache
    end
    -- FIX v1.4: pre-allocate بدل table.insert المتكرر
    local all = {}
    local idx = 0
    for _, svc in ipairs(SEARCH_SERVICES) do
        if svc and type(svc) == "userdata" then
            local desc = svc:GetDescendants()
            for i = 1, #desc do
                idx = idx + 1
                all[idx] = desc[i]
            end
        end
    end
    -- FIX v1.4: فقط اللاعب المحلي (بدل كل اللاعبين) لأن هذا LocalScript
    local pg = player:FindFirstChild("PlayerGui")
    if pg then
        local desc = pg:GetDescendants()
        for i = 1, #desc do
            idx = idx + 1
            all[idx] = desc[i]
        end
    end
    _searchCache = all
    _searchCacheTime = tick()
    return all
end

local function findByName(search)
    if not search or search == "" then return nil end
    if search:find(".", 1, true) then
        local parts = {}
        for p in search:gmatch("[^.]+") do table.insert(parts, p) end
        if #parts >= 2 then
            local function traverseFrom(start, startIdx)
                local current = start
                for i = startIdx, #parts do
                    local child = current:FindFirstChild(parts[i])
                    if not child then
                        child = current:FindFirstChild(parts[i]:lower())
                    end
                    if not child then
                        for _, c in ipairs(current:GetChildren()) do
                            if c.Name:lower() == parts[i]:lower() then
                                child = c
                                break
                            end
                        end
                    end
                    if not child then
                        local searchNoSpaces = parts[i]:gsub("%s+", "")
                        for _, c in ipairs(current:GetChildren()) do
                            if c.Name:lower():gsub("%s+", "") == searchNoSpaces:lower() then
                                child = c
                                break
                            end
                        end
                    end
                    if not child then return nil end
                    if i == #parts then return child end
                    current = child
                end
                return nil
            end
            -- try direct service lookup
            local svc = PARENT_MAP[parts[1]]
            if svc then
                local found = traverseFrom(svc, 2)
                if found then return found end
            end
            -- parts[1] might match a service key case-insensitively
            for svcKey, svcVal in pairs(PARENT_MAP) do
                if svcKey:lower() == parts[1]:lower() then
                    local found = traverseFrom(svcVal, 2)
                    if found then return found end
                end
            end
            -- parts[1] is not a service; search across all services for the root
            for _, svc in ipairs(SEARCH_SERVICES) do
                if svc and type(svc) == "userdata" then
                    local root = svc:FindFirstChild(parts[1])
                    if not root then
                        for _, c in ipairs(svc:GetChildren()) do
                            if c.Name:lower() == parts[1]:lower() then
                                root = c
                                break
                            end
                        end
                    end
                    if root then
                        local found = traverseFrom(root, 2)
                        if found then return found end
                        if root:IsA("Player") and root.Character then
                            found = traverseFrom(root.Character, 2)
                            if found then return found end
                        end
                    end
                end
            end
        end
    end
    local lowerSearch = search:lower()
    local exact, prefixMatch, substringMatch = nil, nil, nil
    for _, obj in ipairs(searchAllDescendants()) do
        local name = obj.Name:lower()
        if name == lowerSearch then
            exact = obj
            break
        elseif not prefixMatch and name:find(lowerSearch, 1, true) == 1 then
            prefixMatch = obj
        elseif not substringMatch and name:find(lowerSearch, 1, true) then
            substringMatch = obj
        end
    end
    return exact or prefixMatch or substringMatch
end

local function findAllByName(search)
    local results = {}
    if not search or search == "" then return results end
    local lowerSearch = search:lower()
    local cache = _searchCache
    if cache then
        for _, obj in ipairs(cache) do
            if obj.Name:lower():find(lowerSearch, 1, true) then
                table.insert(results, obj)
            end
        end
    else
        for _, obj in ipairs(searchAllDescendants()) do
            if obj.Name:lower():find(lowerSearch, 1, true) then
                table.insert(results, obj)
            end
        end
    end
    return results
end

local function resolveUDim2(t)
    if type(t) ~= "table" then return nil end
    local xs, xo, ys, yo = t[1], t[2], t[3], t[4]
    if xs == nil and t.X ~= nil then xs = t.X end
    if xo == nil and t.XOffset ~= nil then xo = t.XOffset end
    if ys == nil and t.Y ~= nil then ys = t.Y end
    if yo == nil and t.YOffset ~= nil then yo = t.YOffset end
    if xs == nil then xs = 0 end
    if xo == nil then xo = 0 end
    if ys == nil then ys = 0 end
    if yo == nil then yo = 0 end
    return UDim2.new(xs, xo, ys, yo)
end

local function applyProperties(inst, props)
    if type(props) ~= "table" then return end
    for key, value in pairs(props) do
        pcall(function()
            if key == "Position" and type(value) == "table" then
                if inst:IsA("GuiObject") then
                    local u = resolveUDim2(value)
                    if u then inst.Position = u end
                else
                    inst.Position = Vector3.new(value[1] or 0, value[2] or 0, value[3] or 0)
                end
            elseif key == "Size" and type(value) == "table" then
                if inst:IsA("GuiObject") then
                    local u = resolveUDim2(value)
                    if u then inst.Size = u end
                elseif inst:IsA("BasePart") then
                    inst.Size = Vector3.new(value[1] or 1, value[2] or 1, value[3] or 1)
                else
                    inst.Size = UDim2.new(value[1] or 0, value[2] or 0, value[3] or 0, value[4] or 0)
                end
            elseif key == "Color" and type(value) == "table" then
                inst.Color = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
            elseif key == "TextColor3" and type(value) == "table" then
                inst.TextColor3 = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
            elseif key == "BackgroundColor3" and type(value) == "table" then
                inst.BackgroundColor3 = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
            elseif key == "BorderColor3" and type(value) == "table" then
                inst.BorderColor3 = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
            elseif key == "BrickColor" and type(value) == "string" then
                inst.BrickColor = BrickColor.new(value)
            elseif key == "Material" and type(value) == "string" then
                inst.Material = Enum.Material[value] or inst.Material
            elseif key == "Shape" and type(value) == "string" then
                if inst:IsA("Part") then
                    inst.Shape = Enum.PartType[value] or Enum.PartType.Block
                end
            elseif key == "MeshId" and type(value) == "string" then
                if inst:IsA("MeshPart") then
                    inst.MeshId = value
                elseif inst:IsA("Part") then
                    local mesh = Instance.new("SpecialMesh")
                    mesh.MeshId = value
                    mesh.Parent = inst
                end
            elseif key == "TextureId" and type(value) == "string" then
                if inst:IsA("MeshPart") then
                    inst.TextureId = value
                elseif inst:IsA("Part") then
                    local mesh = inst:FindFirstChildOfClass("SpecialMesh") or Instance.new("SpecialMesh")
                    mesh.TextureId = value
                    mesh.Parent = inst
                end
            elseif key == "Font" and type(value) == "string" then
                inst.Font = Enum.Font[value] or inst.Font
            elseif key == "TextXAlignment" and type(value) == "string" then
                inst.TextXAlignment = Enum.TextXAlignment[value] or Enum.TextXAlignment.Center
            elseif key == "TextYAlignment" and type(value) == "string" then
                inst.TextYAlignment = Enum.TextYAlignment[value] or Enum.TextYAlignment.Center
            elseif key == "AnchorPoint" and type(value) == "table" then
                inst.AnchorPoint = Vector2.new(value[1] or 0, value[2] or 0)
            elseif key == "Image" and type(value) == "string" then
                if inst:IsA("ImageLabel") or inst:IsA("ImageButton") then
                    inst.Image = value
                end
            elseif key == "Anchored" then
                inst.Anchored = value
            elseif key == "Transparency" then
                inst.Transparency = value
            elseif key == "BackgroundTransparency" then
                inst.BackgroundTransparency = value
            elseif key == "CanCollide" then
                inst.CanCollide = value
            elseif key == "WalkSpeed" then
                inst.WalkSpeed = value
            elseif key == "PlatformStand" then
                inst.PlatformStand = value
            elseif key == "Brightness" then
                inst.Brightness = value
            elseif key == "ClockTime" then
                inst.ClockTime = value
            elseif key == "FogEnd" then
                inst.FogEnd = value
            elseif key == "FogStart" then
                inst.FogStart = value
            elseif key == "Source" then
                local srcOk = pcall(function() inst.Source = "--from skill-Ai\n" .. tostring(value) end)
                if not srcOk then
                    local srcOk2 = pcall(function() inst[key] = "--from skill-Ai\n" .. tostring(value) end)
                end
            else
                inst[key] = value
            end
        end)
    end
end

local function safeRandomRange(r)
    local function pick(arrIdx, objKey, default)
        local v
        if type(r) == "table" then
            v = r[arrIdx]
            if v == nil and objKey then v = r[objKey] end
        end
        v = tonumber(v)
        if v == nil then v = default end
        return math.floor(v)
    end

    local minX = pick(1, "minX", -60)
    local maxX = pick(2, "maxX", 60)
    local minY = pick(3, "minY", -5)
    local maxY = pick(4, "maxY", 10)
    local minZ = pick(5, "minZ", -60)
    local maxZ = pick(6, "maxZ", 60)

    if minX > maxX then minX, maxX = maxX, minX end
    if minY > maxY then minY, maxY = maxY, minY end
    if minZ > maxZ then minZ, maxZ = maxZ, minZ end

    return minX, maxX, minY, maxY, minZ, maxZ
end

local function getOrCreateContainer(name, parent)
    if not name or name == "" then return parent end
    local existing = parent:FindFirstChild(name)
    if existing and existing:IsA("Folder") then
        return existing
    end
    local folder = Instance.new("Folder")
    folder.Name = name
    folder.Parent = parent
    return folder
end

local function makeScriptWithBackup(className, name, source, targetParent, containerName)
    if containerName then
        targetParent = getOrCreateContainer(containerName, targetParent)
    end

    local host = Instance.new("StringValue")
    host.Name = name or className
    host.Value = source
    host.Parent = targetParent

    local mock = {
        Name = host.Name,
        Parent = targetParent,
        ClassName = className,
        Source = source,
        Value = source,
        FindFirstChild = function(self, childName)
            return targetParent:FindFirstChild(childName)
        end,
        WaitForChild = function(self, childName, timeout)
            return targetParent:WaitForChild(childName, timeout)
        end,
        IsA = function(self, class)
            return className == class
        end,
        Destroy = function(self)
            host:Destroy()
        end,
    }

    local loadOk = false
    local compileErr = nil
    pcall(function()
        local fn, compErr = loadstring("local script = ...\n" .. source)
        if fn then
            task.spawn(function()
                local runtimeOk, runtimeErr = pcall(fn, mock)
                if not runtimeOk then
                    logToFile("Runtime error in " .. (name or className), runtimeErr)
                end
            end)
            loadOk = true
        else
            compileErr = compErr
            logToFile("Compile error in " .. (name or className), compErr)
        end
    end)

    return mock, nil, true, host, loadOk, compileErr
end

local function ensureToolHandle(toolInst, handleProps)
    local handle = toolInst:FindFirstChild("Handle")
    if not handle then
        handle = Instance.new("Part")
        handle.Name = "Handle"
        handle.Size = Vector3.new(0.6, 2.5, 0.6)
        handle.Color = Color3.fromRGB(150, 150, 150)
        handle.Material = Enum.Material.Metal
        handle.Parent = toolInst
    end
    if type(handleProps) == "table" then
        applyProperties(handle, handleProps)
    end
    handle.Anchored = false
    handle.CanCollide = false
    toolInst.RequiresHandle = true
    toolInst.Grip = toolInst.Grip or CFrame.new()
    return handle
end

local function generateChallengeScript(challengeType, targetCount, challengeName, title)
    local function fill(t)
        local s = t
        s = s:gsub("{N}", function() return challengeName end)
        s = s:gsub("{C}", function() return tostring(targetCount) end)
        s = s:gsub("{T}", function() return (title or "Challenge") end)
        return s
    end
    if challengeType == "reach" then
        return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false

detector.Touched:Connect(function(hit)
    if completed then return end
    local char = hit.Parent
    local hum = char and char:FindFirstChild("Humanoid")
    if hum and plr.Character == char then
        completed = true
        progressLabel.Text = "1/1"
        progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
                titleLabel.Text = "✅ REACHED!"
    end
end)
]])
    elseif challengeType == "survive" then
        return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false
local inside = false
local survivedSeconds = 0
local TARGET = {C}

detector.Touched:Connect(function(hit)
    local char = hit.Parent
    local hum = char and char:FindFirstChild("Humanoid")
    if hum and plr.Character == char then inside = true end
end)
detector.TouchEnded:Connect(function(hit)
    local char = hit.Parent
    local hum = char and char:FindFirstChild("Humanoid")
    if hum and plr.Character == char then inside = false end
end)

task.spawn(function()
    while not completed do
        task.wait(1)
        if inside then
            survivedSeconds = survivedSeconds + 1
            progressLabel.Text = survivedSeconds .. "/" .. TARGET
            if survivedSeconds >= TARGET then
                completed = true
                progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
                titleLabel.Text = "✅ SURVIVED!"
            end
        end
    end
end)
]])
    elseif challengeType == "kill" then
        return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false
local killCount = 0
local connectedHumans = {}
local TARGET = {C}

detector.Touched:Connect(function(hit)
    local char = hit.Parent
    local hum = char and char:FindFirstChild("Humanoid")
    if hum and not connectedHumans[hum] then
        connectedHumans[hum] = true
        hum.Died:Connect(function()
            if completed then return end
            if not char.Parent then
                task.wait(0.1)
                killCount = killCount + 1
                progressLabel.Text = killCount .. "/" .. TARGET
                if killCount >= TARGET then
                    completed = true
                    progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
                    titleLabel.Text = "✅ COMPLETE!"
                end
            end
        end)
    end
end)
]])
    elseif challengeType == "collect" then
        return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false
local collected = 0
local TARGET = {C}

detector.Touched:Connect(function(hit)
    if completed then return end
    if hit:GetAttribute("Collectible") or hit.Name:find("Collect") then
        collected = collected + 1
        progressLabel.Text = collected .. "/" .. TARGET
        pcall(function() hit:Destroy() end)
        if collected >= TARGET then
            completed = true
            progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
            titleLabel.Text = "✅ COLLECTED!"
        end
    end
end)
]])
    end
    return "task.wait(9e9)"
end

local function setCollision(inst, enabled)
    if not inst or not inst:IsA("BasePart") then return end
    inst.CanCollide = enabled
    task.spawn(function()
        task.wait(0)
        if inst and inst.Parent then
            inst.CanCollide = enabled
        end
    end)
    task.spawn(function()
        task.wait(0.15)
        if inst and inst.Parent then
            inst.CanCollide = enabled
        end
    end)
end

local function executeAction(action)
    local kind = action.type

    if kind == "create_instance" then
        local className = action.className
        if not className then return false, "❌ Missing className" end

        local ok, inst = pcall(function() return Instance.new(className) end)
        if not ok or not inst then
            return false, "❌ Failed to create " .. tostring(className)
        end

        inst.Name = action.name or className
        applyProperties(inst, action.properties)

        -- set Anchored/CanCollide BEFORE parent (belt)
        if inst:IsA("BasePart") and className ~= "Tool" then
            inst.Anchored = true
            setCollision(inst, true)
        end

        if className == "Tool" then
            ensureToolHandle(inst, action.handleProperties)
        end

        local targetParent = resolveParent(action.parent, Workspace)

        -- AUTO-CORRECT: GUI objects in Workspace belong in StarterGui
        local GUI_CLASSES = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
            ImageLabel = true, ImageButton = true, ScrollingFrame = true, BillboardGui = true, ViewportFrame = true }
        if GUI_CLASSES[className] and targetParent == Workspace then
            targetParent = StarterGui
        end
        -- AUTO-CORRECT: Parts don't belong in GUI services
        local GUI_SERVICES = { StarterGui = true, StarterPlayerScripts = true, StarterCharacterScripts = true }
        if inst:IsA("BasePart") and GUI_SERVICES[targetParent.Name] then
            targetParent = Workspace
        end
        -- AUTO-CORRECT: Decal/Texture/SurfaceGui need a BasePart parent, not Workspace
        local ATTACHMENT_CLASSES = { Decal = true, Texture = true, SurfaceGui = true }
        if ATTACHMENT_CLASSES[className] and targetParent == Workspace then
            for i = #_batchCreated, 1, -1 do
                local prev = _batchCreated[i]
                if type(prev) ~= "table" and prev:IsA("BasePart") then
                    targetParent = prev
                    break
                end
            end
        end
        -- AUTO-CORRECT: StringValue/ModuleScript with code belong in ServerStorage, not Workspace
        local CODE_CLASSES = { StringValue = true, ModuleScript = true }
        if CODE_CLASSES[className] and targetParent == Workspace then
            targetParent = ServerStorage
        end

        if action.containerName then
            targetParent = getOrCreateContainer(action.containerName, targetParent)
        end
        inst.Parent = targetParent

        -- force Anchored/CanCollide AFTER parent (suspenders — some executors lose pre-parent writes)
        if inst:IsA("BasePart") then
            if className == "Tool" then
                local props = type(action.properties) == "table" and action.properties or {}
                if props.Anchored == nil then inst.Anchored = false end
                if props.CanCollide == nil then setCollision(inst, false) end
            else
                inst.Anchored = true
                setCollision(inst, true)
            end
        end
        table.insert(_batchCreated, inst)
        updateSceneState(action, true)

        if className == "Camera" and action.setAsCurrentCamera then
            Workspace.CurrentCamera = inst
            local hrp = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
            if hrp then
                local offset = action.cameraOffset or {0, 2, -10}
                inst.CFrame = CFrame.new(
                    hrp.Position + Vector3.new(0, 2, 0),
                    hrp.Position + Vector3.new(offset[1] or 0, offset[2] or 2, offset[3] or -10)
                )
            end
        end

        _lastClickInstance = inst
        return true, "✅ Created " .. inst.Name .. " (" .. className .. ")"

    elseif kind == "create_multiple" then
        local className = action.className or "Part"
        local count = math.clamp(tonumber(action.count) or 1, 1, 200)
        local parent = resolveParent(action.parent, Workspace)
        -- AUTO-CORRECT: bulk create is for Parts, redirect from GUI services
        local GUI_SERVICES = { StarterGui = true, StarterPlayerScripts = true, StarterCharacterScripts = true }
        if GUI_SERVICES[parent.Name] then
            parent = Workspace
        end
        local container = getOrCreateContainer(action.containerName or (className .. "_Batch"), parent)

        local created = 0
        for i = 1, count do
            pcall(function()
                local ok, inst = pcall(function() return Instance.new(className) end)
                if ok and inst then
                    inst.Name = (action.namePrefix or className) .. "_" .. i
                    applyProperties(inst, action.properties)
                    -- belt: set before parent
                    if inst:IsA("BasePart") and className ~= "Tool" then
                        inst.Anchored = true
                        setCollision(inst, true)
                    end
                    if className == "Tool" then
                        ensureToolHandle(inst, action.handleProperties)
                    end
                    if action.randomizePosition then
                        local minX, maxX, minY, maxY, minZ, maxZ = safeRandomRange(action.randomRange)
                        if action.gridSpacing then
                            local spacing = math.max(1, tonumber(action.gridSpacing) or 4)
                            local maxAttempts = 30
                            local placed = _batchPlacedGrid or {}
                            _batchPlacedGrid = placed
                            local found = false
                            for _ = 1, maxAttempts do
                                local gx = math.floor(math.random(minX, maxX) / spacing) * spacing
                                local gy = math.floor(math.random(minY, maxY) / spacing) * spacing
                                local gz = math.floor(math.random(minZ, maxZ) / spacing) * spacing
                                local key = gx..","..gy..","..gz
                                if not placed[key] then
                                    placed[key] = true
                                    inst.Position = Vector3.new(gx, gy, gz)
                                    found = true
                                    break
                                end
                            end
                            if not found then
                                inst.Position = Vector3.new(math.random(minX, maxX), math.random(minY, maxY), math.random(minZ, maxZ))
                            end
                        else
                            inst.Position = Vector3.new(
                                math.random(minX, maxX),
                                math.random(minY, maxY),
                                math.random(minZ, maxZ)
                            )
                        end
                        -- rejection sampling: if rejectionRadius set, check distance to existing BaseParts in container
                        if action.rejectionRadius and container then
                            local radius = tonumber(action.rejectionRadius) or 4
                            local rejectAttempts = 30
                            for _ = 1, rejectAttempts do
                                local tooClose = false
                                for _, existing in ipairs(container:GetDescendants()) do
                                    if existing:IsA("BasePart") and existing ~= inst then
                                        local dist = (inst.Position - existing.Position).Magnitude
                                        if dist < radius then
                                            tooClose = true
                                            break
                                        end
                                    end
                                end
                                if tooClose then
                                    inst.Position = Vector3.new(
                                        math.random(minX, maxX),
                                        math.random(minY, maxY),
                                        math.random(minZ, maxZ)
                                    )
                                else
                                    break
                                end
                            end
                        end
                    end
                    inst.Parent = container
                    -- force Anchored/CanCollide AFTER parenting for physics engine
                    if inst:IsA("BasePart") then
                        if className == "Tool" then
                            local props = type(action.properties) == "table" and action.properties or {}
                            if props.Anchored == nil then inst.Anchored = false end
                            if props.CanCollide == nil then setCollision(inst, false) end
                        else
                            inst.Anchored = true
                            setCollision(inst, true)
                        end
                    end
                    table.insert(_batchCreated, inst)
                    created += 1
                end
            end)
        end
        _batchPlacedGrid = nil
        updateSceneState(action, true)
        _lastClickInstance = container
        return true, "✅ Created " .. created .. " items inside " .. container.Name

    elseif kind == "set_property" then
        local target = action.target and findByName(action.target)
        if not target then return false, "❌ Not found: " .. tostring(action.target) end
        applyProperties(target, action.properties)
        updateSceneState(action, true)
        return true, "✅ Updated properties: " .. target.Name

    elseif kind == "delete" then
        local target = action.target and findByName(action.target)
        if not target then return false, "❌ Not found: " .. tostring(action.target) end
        local n = target.Name
        table.insert(_batchCreated, { _markedForDestroy = true, _ref = target, _name = n })
        target:Destroy()
        updateSceneState(action, true)
        return true, "🗑️ Deleted: " .. n

    elseif kind == "clone" then
        local target = action.target and findByName(action.target)
        if not target then return false, "❌ Not found: " .. tostring(action.target) end
        local clone = target:Clone()
        clone.Name = action.newName or (target.Name .. "_clone")
        clone.Parent = target.Parent
        table.insert(_batchCreated, clone)
        updateSceneState(action, true)
        return true, "📋 Cloned: " .. target.Name .. " → " .. clone.Name

    elseif kind == "rename" then
        local target = action.target and findByName(action.target)
        if not target then return false, "❌ Not found: " .. tostring(action.target) end
        local old = target.Name
        target.Name = action.newName or old
        updateSceneState(action, true)
        return true, "✏️ Renamed: " .. old .. " → " .. target.Name

    elseif kind == "move" then
        local target = action.target and findByName(action.target)
        if not target then return false, "❌ Not found: " .. tostring(action.target) end
        if target:IsA("BasePart") and action.position then
            target.Position = Vector3.new(action.position[1] or 0, action.position[2] or 0, action.position[3] or 0)
            updateSceneState(action, true)
            return true, "🚚 Moved: " .. target.Name
        elseif action.parent then
            target.Parent = resolveParent(action.parent, target.Parent)
            updateSceneState(action, true)
            return true, "🚚 Moved: " .. target.Name .. " to " .. tostring(action.parent)
        end
        return false, "❌ Insufficient move data"

    elseif kind == "set_lighting" then
        if type(action.properties) == "table" and action.properties.FogEnd ~= nil then
            local fogKeywords = { "fog", "backrooms", "horror", "mist", "haze", "creepy" }
            local mentioned = false
            local lowerQ = (CURRENT_REQUEST_TEXT or ""):lower()
            for _, kw in ipairs(fogKeywords) do
                if lowerQ:find(kw, 1, true) then
                    mentioned = true
                    break
                end
            end
            if not mentioned then
                local fe = tonumber(action.properties.FogEnd)
                if fe and fe < 300 then
                    action.properties.FogEnd = nil -- skip fog since user did not ask for it
                end
            end
        end
        applyProperties(Lighting, action.properties)
        updateSceneState(action, true)
        return true, "💡 Lighting updated"

    elseif kind == "set_character" then
        local char = player.Character
        if not char or not char:FindFirstChild("Humanoid") then
            return false, "❌ No character available"
        end
        applyProperties(char.Humanoid, action.properties)
        if char:FindFirstChild("HumanoidRootPart") and action.transparency ~= nil then
            char.HumanoidRootPart.Transparency = action.transparency
        end
        updateSceneState(action, true)
        return true, "✅ Character state updated"

    elseif kind == "create_compound" then
        local parts = action.parts
        if not parts or type(parts) ~= "table" or #parts == 0 then
            return false, "❌ Missing parts array for compound"
        end
        local model = Instance.new("Model")
        model.Name = action.name or "Compound"
        local createdParts = {}
        local primaryIdx = action.primaryPartIndex or 1
        local isToolCompound = action.makeTool or action.className == "Tool"

        -- parent model FIRST so parts enter DataModel immediately
        local targetParent = resolveParent(action.parent, Workspace)
        if action.containerName then
            targetParent = getOrCreateContainer(action.containerName, targetParent)
        end
        model.Parent = targetParent

        for i, partData in ipairs(parts) do
            pcall(function()
                local cn = partData.className or "Part"
                local ok, inst = pcall(function() return Instance.new(cn) end)
                if ok and inst then
                    inst.Name = partData.name or ("Part_" .. i)
                    applyProperties(inst, partData)
                    -- belt: set before parent
                    if inst:IsA("BasePart") and not isToolCompound then
                        inst.Anchored = true
                        setCollision(inst, true)
                    end
                    inst.Parent = model
                    table.insert(createdParts, inst)
                end
            end)
        end
        if #createdParts == 0 then
            model:Destroy()
            return false, "❌ Failed to create any compound part"
        end

        -- force Anchored/CanCollide AFTER all parts are in the DataModel
        for _, part in ipairs(createdParts) do
            if part:IsA("BasePart") then
                if isToolCompound then
                    part.Anchored = false
                    setCollision(part, false)
                else
                    part.Anchored = true
                    setCollision(part, true)
                end
            end
        end

        -- ⚠️ فحص تصادم/تراكب بين أجزاء نفس الـ compound: لو الـ AI أخطأ بحساب
        -- position وحط جزئين (مثلاً النصل والمقبض) بنفس النقطة تقريباً، الشكل
        -- النهائي يطلع كتلة واحدة مشوهة بدل سيف/برج واضح المعالم. نتحقق من كل
        -- زوج أجزاء، ولو متراكبين فعلياً (مسافة أقل من نص متوسط حجمهم)، نزيح
        -- الجزء الثاني قليلاً على المحور Y حتى يصير الشكل مفهوم بصرياً.
        local overlapFixCount = 0
        for i = 1, #createdParts do
            for j = i + 1, #createdParts do
                local a, b = createdParts[i], createdParts[j]
                local okCheck, isOverlap = pcall(function()
                    local dist = (a.Position - b.Position).Magnitude
                    local avgSize = ((a.Size.X + a.Size.Y + a.Size.Z) + (b.Size.X + b.Size.Y + b.Size.Z)) / 6
                    return dist < (avgSize * 0.15) -- متراكبين فعلياً، مو بس قريبين بشكل طبيعي
                end)
                if okCheck and isOverlap then
                    pcall(function()
                        b.Position = b.Position + Vector3.new(0, (b.Size.Y * 0.6) + 0.1, 0)
                    end)
                    overlapFixCount = overlapFixCount + 1
                end
            end
        end
        local primary = createdParts[primaryIdx]
        for i, part in ipairs(createdParts) do
            if part ~= primary then
                local weld = Instance.new("WeldConstraint")
                weld.Name = ("Weld_%d_%d"):format(primaryIdx, i)
                weld.Part0 = primary
                weld.Part1 = part
                weld.Parent = part
            end
        end
        model.PrimaryPart = primary
        if action.makeTool or action.className == "Tool" then
            local tool = Instance.new("Tool")
            tool.Name = action.name or model.Name
            local h = Instance.new("Part")
            h.Name = "Handle"
            h.Size = Vector3.new(0.6, 2.5, 0.6)
            h.Anchored = false
            h.CanCollide = false
            h.Parent = tool
            for _, child in ipairs(model:GetChildren()) do
                child.Parent = tool
            end
            model:Destroy()
            model = tool
            tool.Parent = targetParent  -- re-parent since model was destroyed
            tool.RequiresHandle = true
            tool.Grip = CFrame.new()
        end
        table.insert(_batchCreated, model)
        updateSceneState(action, true)
        _lastClickInstance = model
        local overlapMsg = overlapFixCount > 0 and (" (auto-fixed " .. overlapFixCount .. " overlapping part(s))") or ""
        return true, "✅ Created compound: " .. model.Name .. overlapMsg

    elseif kind == "create_enclosure" then
        -- 🧮 GEOMETRY HELPER: يبني أرضية + جدران (وحاجز اختياري) من وصف منطقي
        -- بسيط فقط (مركز، عرض، عمق، ارتفاع) — كل حساب المواقع/المسافات يصير
        -- هنا بالكود (فوري، صفر توكنز)، مو من الـ AI. هذا يلغي السبب الجذري
        -- وراء "Impure JSON" بطلبات فيها جدران/حواجز متعددة المواقع، لأن الموديل
        -- ما يحتاج يحسب إحداثيات يدوياً بصوت عالي قبل الرد.
        local centerX = tonumber(action.centerX) or 0
        local centerY = tonumber(action.centerY) or 0
        local centerZ = tonumber(action.centerZ) or 0
        local width = math.max(2, tonumber(action.width) or 20)   -- X axis
        local depth = math.max(2, tonumber(action.depth) or 20)   -- Z axis
        local wallHeight = math.max(1, tonumber(action.wallHeight) or 4)
        local floorThickness = 0.5
        local wallThickness = 0.5
        local hasFloor = action.floor ~= false
        local openSides = {} -- e.g. {"North"} to leave an entrance gap
        if type(action.openSides) == "table" then
            for _, s in ipairs(action.openSides) do openSides[s] = true end
        end
        local color = action.color or {150,150,150}
        local material = action.material or "SmoothPlastic"
        local barrier = action.barrier -- bool: add an invisible solid barrier covering the whole footprint at wall height (used for "locked until unlocked" platforms)

        local model = Instance.new("Model")
        model.Name = action.name or "Enclosure"
        local targetParent = resolveParent(action.parent, Workspace)
        if action.containerName then
            targetParent = getOrCreateContainer(action.containerName, targetParent)
        end
        model.Parent = targetParent

        local function mkPart(pname, size, pos)
            local p = Instance.new("Part")
            p.Name = pname
            p.Size = Vector3.new(size[1], size[2], size[3])
            p.Position = Vector3.new(pos[1], pos[2], pos[3])
            pcall(function() p.Color = Color3.fromRGB(color[1] or 150, color[2] or 150, color[3] or 150) end)
            pcall(function() p.Material = Enum.Material[material] or Enum.Material.SmoothPlastic end)
            p.Anchored = true
            setCollision(p, true)
            p.Parent = model
            return p
        end

        if hasFloor then
            mkPart("Floor", {width, floorThickness, depth}, {centerX, centerY, centerZ})
        end
        local floorTop = centerY + floorThickness / 2
        local wallCenterY = floorTop + wallHeight / 2
        if not openSides["North"] then
            mkPart("WallNorth", {width, wallHeight, wallThickness}, {centerX, wallCenterY, centerZ - depth/2})
        end
        if not openSides["South"] then
            mkPart("WallSouth", {width, wallHeight, wallThickness}, {centerX, wallCenterY, centerZ + depth/2})
        end
        if not openSides["East"] then
            mkPart("WallEast", {wallThickness, wallHeight, depth}, {centerX + width/2, wallCenterY, centerZ})
        end
        if not openSides["West"] then
            mkPart("WallWest", {wallThickness, wallHeight, depth}, {centerX - width/2, wallCenterY, centerZ})
        end

        if barrier then
            local b = mkPart("Barrier", {width, wallHeight, depth}, {centerX, wallCenterY, centerZ})
            b.Transparency = 1
            setCollision(b, true)
            b.Name = "Barrier"
        end

        table.insert(_batchCreated, model)
        updateSceneState(action, true)
        _lastClickInstance = model
        return true, "✅ Created enclosure: " .. model.Name .. " (" .. (hasFloor and "floor+" or "") .. "walls" .. (barrier and "+barrier" or "") .. ")"

    elseif kind == "create_challenge_system" then
        local challengeType = (action.challengeType or "reach"):lower()
        local validTypes = { reach = true, survive = true, kill = true, collect = true }
        if not validTypes[challengeType] then
            return false, "❌ Invalid challengeType '" .. tostring(action.challengeType) .. "'. Valid: reach, survive, kill, collect"
        end
        local targetCount = math.max(1, tonumber(action.targetCount) or 10)
        local title = action.title or ("Challenge: " .. challengeType)
        local challengeName = action.name or "Challenge"
        local centerX = tonumber(action.centerX) or 0
        local centerY = tonumber(action.centerY) or 5
        local centerZ = tonumber(action.centerZ) or 0
        local width = math.max(2, tonumber(action.width) or 20)
        local depth = math.max(2, tonumber(action.depth) or 20)
        local height = math.max(2, tonumber(action.height) or 6)
        local visibleFloor = action.visibleFloor ~= false

        local zoneModel = Instance.new("Model")
        zoneModel.Name = challengeName .. "_Zone"
        local zoneParent = resolveParent(action.parent, Workspace)
        if action.containerName then
            zoneParent = getOrCreateContainer(action.containerName, zoneParent)
        end
        zoneModel.Parent = zoneParent

        if visibleFloor then
            local floor = Instance.new("Part")
            floor.Name = challengeName .. "_Floor"
            floor.Size = Vector3.new(width, 0.5, depth)
            floor.Position = Vector3.new(centerX, centerY - 0.25, centerZ)
            floor.Anchored = true
            setCollision(floor, true)
            floor.BrickColor = BrickColor.new("Bright blue")
            floor.Material = Enum.Material.SmoothPlastic
            floor.Transparency = 0.3
            floor.Parent = zoneModel
        end

        -- optional walls around zone
        local wallOpenSides = {}
        if type(action.walls) == "table" then
            for _, s in ipairs(action.walls) do wallOpenSides[s] = true end
        end
        if action.walls == true or type(action.walls) == "table" then
            local wallH = math.max(1, tonumber(action.wallHeight) or height)
            local wallT = 0.5
            local floorTop = centerY + 0.25
            local wallCY = floorTop + wallH / 2
            local wallColor = action.wallColor or {100, 100, 255}
            local wallMat = action.wallMaterial or "SmoothPlastic"
            local function mkWall(n, sz, pos)
                local w = Instance.new("Part")
                w.Name = challengeName .. "_" .. n
                w.Size = Vector3.new(sz[1], sz[2], sz[3])
                w.Position = Vector3.new(pos[1], pos[2], pos[3])
                w.Anchored = true
                setCollision(w, true)
                pcall(function() w.Color = Color3.fromRGB(wallColor[1] or 100, wallColor[2] or 100, wallColor[3] or 255) end)
                pcall(function() w.Material = Enum.Material[wallMat] or Enum.Material.SmoothPlastic end)
                w.Transparency = 0.2
                w.Parent = zoneModel
            end
            if not wallOpenSides["North"] then mkWall("WallNorth", {width, wallH, wallT}, {centerX, wallCY, centerZ - depth/2}) end
            if not wallOpenSides["South"] then mkWall("WallSouth", {width, wallH, wallT}, {centerX, wallCY, centerZ + depth/2}) end
            if not wallOpenSides["East"] then mkWall("WallEast", {wallT, wallH, depth}, {centerX + width/2, wallCY, centerZ}) end
            if not wallOpenSides["West"] then mkWall("WallWest", {wallT, wallH, depth}, {centerX - width/2, wallCY, centerZ}) end
        end

        local detector = Instance.new("Part")
        detector.Name = challengeName .. "_Detector"
        detector.Size = Vector3.new(width, height, depth)
        detector.Position = Vector3.new(centerX, centerY + height / 2 - 0.25, centerZ)
        detector.Anchored = true
        detector.CanCollide = false
        detector.Transparency = 1
        detector.Parent = zoneModel

        table.insert(_batchCreated, zoneModel)

        local guiPosition = (action.guiPosition or "top-right"):lower()
        local guiPosMap = {
            ["top-right"] = UDim2.new(1, -270, 0, 10),
            ["top-left"] = UDim2.new(0, 20, 0, 10),
            ["bottom-right"] = UDim2.new(1, -270, 1, -80),
            ["bottom-left"] = UDim2.new(0, 20, 1, -80),
            ["center"] = UDim2.new(0.5, -125, 0.5, -30),
        }
        local guiSize = type(action.guiSize) == "table" and action.guiSize or {250, 60}
        local gw = math.max(100, tonumber(guiSize[1]) or 250)
        local gh = math.max(40, tonumber(guiSize[2]) or 60)

        local gui = Instance.new("ScreenGui")
        gui.Name = challengeName .. "_GUI"
        gui.ResetOnSpawn = false
        gui.Parent = StarterGui

        local frame = Instance.new("Frame")
        frame.Name = "MainFrame"
        frame.Size = UDim2.new(0, gw, 0, gh)
        frame.Position = guiPosMap[guiPosition] or guiPosMap["top-right"]
        frame.BackgroundColor3 = Color3.fromRGB(10, 10, 20)
        frame.BackgroundTransparency = 0.3
        frame.BorderSizePixel = 0
        frame.Parent = gui
        local uic = Instance.new("UICorner")
        uic.CornerRadius = UDim.new(0, 8)
        uic.Parent = frame

        local titleLabel = Instance.new("TextLabel")
        titleLabel.Name = "Title"
        titleLabel.Size = UDim2.new(1, -20, 0, 22)
        titleLabel.Position = UDim2.new(0, 10, 0, 4)
        titleLabel.BackgroundTransparency = 1
        titleLabel.Text = title
        titleLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
        titleLabel.TextSize = 14
        titleLabel.Font = Enum.Font.GothamBold
        titleLabel.TextXAlignment = Enum.TextXAlignment.Left
        titleLabel.Parent = frame

        local progressLabel = Instance.new("TextLabel")
        progressLabel.Name = "Progress"
        progressLabel.Size = UDim2.new(1, -20, 0, 26)
        progressLabel.Position = UDim2.new(0, 10, 0, 28)
        progressLabel.BackgroundTransparency = 1
        progressLabel.Text = "0/" .. targetCount
        progressLabel.TextColor3 = Color3.fromRGB(0, 200, 255)
        progressLabel.TextSize = 20
        progressLabel.Font = Enum.Font.GothamBlack
        progressLabel.TextXAlignment = Enum.TextXAlignment.Left
        progressLabel.Parent = frame

        table.insert(_batchCreated, gui)

        local scriptSrc = generateChallengeScript(challengeType, targetCount, challengeName, title)

        local host = Instance.new("StringValue")
        host.Name = challengeName .. "_Script"
        host.Value = "--from skill-Ai\n" .. scriptSrc
        host.Parent = zoneModel

        local mock = {
            Name = host.Name,
            Parent = zoneModel,
            ClassName = "LocalScript",
            Source = scriptSrc,
            Value = scriptSrc,
            FindFirstChild = function(self, childName)
                local ok, r = pcall(function() return zoneModel:FindFirstChild(childName) end)
                return ok and r
            end,
            WaitForChild = function(self, childName, timeout)
                local ok, r = pcall(function() return zoneModel:WaitForChild(childName, timeout) end)
                return ok and r
            end,
            FindFirstChildOfClass = function(self, cls)
                local ok, r = pcall(function() return zoneModel:FindFirstChildOfClass(cls) end)
                return ok and r
            end,
            Destroy = function(self)
                pcall(function() host:Destroy() end)
                pcall(function() gui:Destroy() end)
                pcall(function() zoneModel:Destroy() end)
                for idx, entry in ipairs(_autoRunScripts) do
                    if entry.inst == self then
                        table.remove(_autoRunScripts, idx)
                        break
                    end
                end
            end,
            IsA = function(self, cls) return cls == "Script" or cls == "LocalScript" end,
        }

        local fn, compileErr = loadstring("local script = ...\n" .. scriptSrc)
        local loadOk = false
        local execErr = nil
        if fn then
            local ok, e = pcall(fn, mock)
            if ok then
                loadOk = true
            else
                execErr = tostring(e)
            end
        else
            execErr = tostring(compileErr)
        end

        if loadOk then
            table.insert(_autoRunScripts, { name = host.Name, source = scriptSrc, inst = mock })
            if #_autoRunScripts > AUTORUN_MAX then
                table.remove(_autoRunScripts, 1)
            end
        end

        updateSceneState(action, true)
        local fx = visibleFloor and "floor+" or ""
        local msg = "✅ Created challenge: " .. challengeName .. " (" .. challengeType .. " " .. targetCount .. ", " .. fx .. "zone)"
        if loadOk then
            msg = msg .. " | Executed"
        elseif execErr then
            msg = msg .. " | ⚠️ " .. execErr
        end
        return true, msg

    elseif kind == "create_script" then
        local className = action.className or "Script"
        if className ~= "Script" and className ~= "LocalScript" then
            return false, "❌ create_script requires className = Script or LocalScript"
        end
        local source = "--from skill-Ai\n"
        if action.source then
            source = source .. action.source
        end

        -- auto-fix syntax errors before creating
        local fn_check, err_check = loadstring("local script = ...\n" .. source)
        if not fn_check then
            local rawCode = action.source or source
            local fixedCode, fixErr = fixLuaCode(rawCode, err_check)
            if fixedCode then
                source = "--from skill-Ai\n" .. fixedCode
            end
        end

        local targetParent = resolveParent(action.parent, nil)
        if not targetParent and type(action.parent) == "string" then
            for _, createdInst in ipairs(_batchCreated) do
                if type(createdInst) ~= "table" and createdInst.Name == action.parent then
                    targetParent = createdInst
                    break
                end
            end
            if not targetParent then
                targetParent = findByName(action.parent)
            end
        end
        targetParent = targetParent or Workspace

        -- SAFETY: reject per-player PlayerGui as parent (scripts there run once per player, confusing)
        if targetParent and targetParent:IsA("PlayerGui") and targetParent.Parent and targetParent.Parent:IsA("Player") then
            targetParent = StarterPlayerScripts or Workspace
        end

        -- AUTO-CORRECT: Scripts in Workspace never run — redirect
        if targetParent == Workspace then
            if className == "LocalScript" then
                targetParent = StarterPlayerScripts or StarterPlayer or Workspace
            else
                targetParent = ServerStorage or Workspace
            end
        end

        local inst, err, sourceWorked, host, loadOk, compileErr = makeScriptWithBackup(
            className, action.name, source, targetParent, action.containerName
        )
        if not inst then
            return false, "❌ " .. err
        end

        table.insert(_batchCreated, host)
        updateSceneState(action, true)
        _lastClickInstance = host

        -- register for respawn re-execution so effects persist across deaths
        if loadOk then
            table.insert(_autoRunScripts, { name = inst.Name, source = source, inst = inst })
            if #_autoRunScripts > AUTORUN_MAX then
                table.remove(_autoRunScripts, 1)
            end
        end

        local parts = {}
        table.insert(parts, "✅ Created: " .. inst.Name .. " (" .. className .. ")")
        if loadOk then
            table.insert(parts, "Executed in-game")
        elseif compileErr then
            table.insert(parts, "⚠️ " .. compileErr)
        end
        local msg = table.concat(parts, " | ")
        msg = msg .. "\n━━━━━━━━━━━━━━━━━━\n" .. source .. "\n━━━━━━━━━━━━━━━━━━"
        return true, msg

    elseif kind == "undo" then
        if #undoStack == 0 then
            return false, "⚠️ Nothing to undo."
        end
        local lastBatch = table.remove(undoStack)
        local removed = 0
        for _, entry in ipairs(lastBatch.instances) do
            if type(entry) == "table" and entry._markedForDestroy then
                -- was a delete action, cannot restore (object already destroyed)
            else
                local inst = entry
                if inst and inst.Parent then
                    inst:Destroy()
                    removed += 1
                end
            end
        end
        return true, "↩️ Undone: " .. lastBatch.summary .. " (removed " .. removed .. " items)"

    elseif kind == "create_plugin" then
        local pluginName = action.name or "Plugin"
        local modules = action.modules
        if not modules or type(modules) ~= "table" or #modules == 0 then
            return false, "❌ create_plugin requires a 'modules' array with at least one {name, source}"
        end
        local icon = action.icon or "🧩"
        local btnColor = action.buttonColor or {30, 30, 40}
        local targetParent = resolveParent(action.parent, CoreGui)

        -- Plugin toolbar GUI
        local toolbar = Instance.new("ScreenGui")
        toolbar.Name = pluginName .. "_Toolbar"
        toolbar.ResetOnSpawn = false
        toolbar.Parent = targetParent

        local bg = Instance.new("Frame")
        bg.Name = "Toolbar"
        bg.Size = UDim2.new(0, #modules * 42 + 12, 0, 38)
        bg.Position = UDim2.new(0.5, -(#modules * 42 + 12) / 2, 0, 80)
        bg.BackgroundColor3 = Color3.fromRGB(btnColor[1] or 30, btnColor[2] or 30, btnColor[3] or 40)
        bg.BackgroundTransparency = 0.2
        bg.BorderSizePixel = 0
        bg.Parent = toolbar
        corner(bg, 8)
        stroke(bg, Color3.fromRGB(0, 200, 255), 1, 0.3)
        makeDraggable(bg)

        local titleLbl = Instance.new("TextLabel")
        titleLbl.Size = UDim2.new(0, 22, 1, 0)
        titleLbl.Position = UDim2.new(0, 4, 0, 0)
        titleLbl.BackgroundTransparency = 1
        titleLbl.Text = icon
        titleLbl.TextColor3 = Color3.fromRGB(0, 200, 255)
        titleLbl.TextSize = 16
        titleLbl.Font = Enum.Font.GothamBold
        titleLbl.Parent = bg

        -- Store module scripts
        local moduleFolder = Instance.new("Folder")
        moduleFolder.Name = pluginName .. "_Modules"
        moduleFolder.Parent = targetParent

        local moduleStates = {} -- track on/off per module
        local activeConnections = {} -- track running threads

        for idx, mod in ipairs(modules) do
            if type(mod) == "table" and mod.name and mod.source then
                -- Store source in StringValue
                local sv = Instance.new("StringValue")
                sv.Name = mod.name
                sv.Value = mod.source
                sv.Parent = moduleFolder

                -- Button for this module
                local btn = Instance.new("TextButton")
                btn.Size = UDim2.new(0, 36, 0, 28)
                btn.Position = UDim2.new(0, 30 + (idx - 1) * 42, 0.5, -14)
                btn.BackgroundColor3 = Color3.fromRGB(50, 50, 60)
                btn.Text = mod.name:sub(1, 2):upper()
                btn.TextColor3 = Color3.fromRGB(180, 180, 190)
                btn.TextSize = 11
                btn.Font = Enum.Font.GothamBold
                btn.Parent = bg
                corner(btn, 6)

                local moduleOn = false
                local thread = nil

                btn.MouseButton1Click:Connect(function()
                    moduleOn = not moduleOn
                    if moduleOn then
                        btn.BackgroundColor3 = Color3.fromRGB(0, 200, 100)
                        btn.TextColor3 = Color3.new(1, 1, 1)
                        -- Execute the module script
                        local fn, err = loadstring("local pluginModule = ...\n" .. mod.source)
                        if fn then
                            local ok, runErr = pcall(fn, { Name = mod.name, Source = mod.source, Parent = moduleFolder })
                            if not ok then
                                addMessage("⚠️ Plugin module '" .. mod.name .. "' error: " .. tostring(runErr), false, true)
                            end
                        else
                            addMessage("⚠️ Plugin module '" .. mod.name .. "' compile error: " .. tostring(err), false, true)
                        end
                        addMessage("✅ Plugin module: " .. mod.name .. " ON", false)
                    else
                        btn.BackgroundColor3 = Color3.fromRGB(50, 50, 60)
                        btn.TextColor3 = Color3.fromRGB(180, 180, 190)
                        addMessage("⏹️ Plugin module: " .. mod.name .. " OFF", false)
                    end
                end)

                moduleStates[mod.name] = false
            end
        end

        table.insert(_batchCreated, toolbar)
        table.insert(_batchCreated, moduleFolder)
        updateSceneState(action, true)
        _lastClickInstance = toolbar
        return true, "✅ Created plugin: " .. pluginName .. " with " .. #modules .. " module(s)"

    elseif kind == "create_particle" then
        local target = resolveTarget(action.target)
        if not target then return false, "❌ create_particle: target '" .. tostring(action.target) .. "' not found" end
        local pe = Instance.new("ParticleEmitter")
        pe.Name = (action.name or "ParticleEmitter")
        pe.Texture = action.texture or "rbxasset://textures/particles/sparkles_main.dds"
        pe.Rate = action.rate or 20
        pe.Speed = action.speed and NumberRange.new(action.speed[1] or 5, action.speed[2] or 10) or NumberRange.new(5, 10)
        pe.Lifetime = action.lifetime and NumberRange.new(action.lifetime[1] or 2, action.lifetime[2] or 4) or NumberRange.new(2, 4)
        if action.size then
            local s = action.size
            pe.Size = NumberSequence.new(NumberSequenceKeypoint.new(0, s[1] or s[1]==nil and 1 or s[1]), NumberSequenceKeypoint.new(1, s[#s] or s[2] or 0.5))
        else
            pe.Size = NumberSequence.new(1, 0.5)
        end
        if action.color then
            local cs = {}
            for i, c in ipairs(action.color) do
                table.insert(cs, ColorSequenceKeypoint.new((i-1)/(#action.color-1), Color3.fromRGB(c[1] or 255, c[2] or 255, c[3] or 255)))
            end
            pe.Color = ColorSequence.new(cs)
        else
            pe.Color = ColorSequence.new(Color3.fromRGB(255, 200, 50))
        end
        pe.Transparency = action.transparency and NumberSequence.new(action.transparency[1] or 0, action.transparency[2] or 1) or NumberSequence.new(0, 1)
        pe.Rotation = action.rotation and NumberRange.new(action.rotation[1] or 0, action.rotation[2] or 360) or NumberRange.new(0, 360)
        if action.spread then pe.SpreadAngle = Vector2.new(action.spread, action.spread) end
        pe.Enabled = if action.enabled ~= nil then action.enabled else true
        pe.Parent = target
        table.insert(_batchCreated, pe)
        updateSceneState(action, true)
        _lastClickInstance = pe
        return true, "✨ Particle emitter added to " .. target.Name

    elseif kind == "create_light" then
        local target = resolveTarget(action.target)
        if not target then return false, "❌ create_light: target '" .. tostring(action.target) .. "' not found" end
        local lightType = action.lightType or "PointLight"
        local light = Instance.new(lightType)
        light.Name = (action.name or lightType)
        light.Color = action.color and Color3.fromRGB(action.color[1] or 255, action.color[2] or 255, action.color[3] or 255) or Color3.new(1, 1, 1)
        light.Brightness = action.brightness or (lightType == "SpotLight" and 2 or 1)
        if light:IsA("PointLight") or light:IsA("SpotLight") then
            light.Range = action.range or 16
        end
        if light:IsA("SpotLight") then
            light.Angle = action.angle or 90
            light.Face = action.Face and Enum.NormalId[action.Face] or Enum.NormalId.Front
        end
        if light:IsA("SurfaceLight") then
            light.Face = action.Face and Enum.NormalId[action.Face] or Enum.NormalId.Front
            light.Range = action.range or 16
        end
        if action.enabled ~= nil then light.Enabled = action.enabled end
        light.Parent = target
        table.insert(_batchCreated, light)
        updateSceneState(action, true)
        _lastClickInstance = light
        return true, "💡 " .. lightType .. " added to " .. target.Name

    elseif kind == "create_sound" then
        local soundId = action.soundId
        if not soundId or soundId == "" then return false, "❌ create_sound requires soundId" end
        local parent
        if action.target then
            parent = resolveTarget(action.target)
            if not parent then return false, "❌ create_sound: target '" .. tostring(action.target) .. "' not found" end
        else
            parent = resolveParent(action.parent, Workspace)
        end
        if not parent then parent = Workspace end
        local sound = Instance.new("Sound")
        sound.Name = action.name or "Sound"
        sound.SoundId = soundId
        sound.Volume = action.volume or 0.5
        sound.Pitch = action.pitch or 1
        sound.Looped = action.loops or false
        if action.startPaused then sound.Playing = false else sound.Playing = true end
        sound.Parent = parent
        table.insert(_batchCreated, sound)
        updateSceneState(action, true)
        _lastClickInstance = sound
        return true, "🔊 Sound created: " .. soundId

    elseif kind == "create_tween" then
        local target = resolveTarget(action.target)
        if not target then return false, "❌ create_tween: target '" .. tostring(action.target) .. "' not found" end
        local props = action.properties
        if not props or next(props) == nil then return false, "❌ create_tween requires a 'properties' table" end
        local duration = action.duration or 1
        local style = action.style or "Quad"
        local direction = action.direction or "Out"
        local tsi = TweenInfo.new(duration, Enum.EasingStyle[style] or Enum.EasingStyle.Quad,
            Enum.EasingDirection[direction] or Enum.EasingDirection.Out, action.repeatCount or 0,
            action.reverses or false)
        local tweenData = {}
        for k, v in pairs(props) do
            if type(v) == "table" and #v == 3 then
                if k:lower():find("color") then
                    pcall(function() tweenData[k] = Color3.fromRGB(v[1] or 255, v[2] or 255, v[3] or 255) end)
                else
                    tweenData[k] = Vector3.new(v[1] or 0, v[2] or 0, v[3] or 0)
                end
            else
                tweenData[k] = v
            end
        end
        local tween = game:GetService("TweenService"):Create(target, tsi, tweenData)
        tween:Play()
        table.insert(_batchCreated, tween)
        updateSceneState(action, true)
        _lastClickInstance = target
        return true, "🎬 Tween started on " .. target.Name .. " (" .. duration .. "s, " .. style .. direction .. ")"

    elseif kind == "create_animation" then
        local animType = action.animationType or "asset"
        if animType == "asset" then
            local targetHum = resolveTarget(action.humanoid or action.target)
            if not targetHum then return false, "❌ create_animation: target humanoid '" .. tostring(action.humanoid or action.target) .. "' not found" end
            local assetId = action.assetId
            if not assetId or assetId == "" then return false, "❌ create_animation: assetId required for 'asset' type" end
            local anim = Instance.new("Animation")
            anim.Name = action.name or "Animation"
            anim.AnimationId = assetId
            local parent = resolveParent(action.parent, targetHum.Parent)
            anim.Parent = parent or targetHum.Parent
            local animator = targetHum:FindFirstChildOfClass("Animator")
            if not animator then animator = Instance.new("Animator"); animator.Parent = targetHum end
            local track = animator:LoadAnimation(anim)
            track:Play()
            if action.loop then track:AdjustSpeed(action.speed or 1)
                track.Looped = true end
            table.insert(_batchCreated, anim)
            updateSceneState(action, true)
            _lastClickInstance = anim
            return true, "🎭 Animation loaded & played on " .. targetHum.Name
        elseif animType == "keyframe" then
            local targetHum = resolveTarget(action.humanoid or action.target)
            if not targetHum then return false, "❌ create_animation: target humanoid '" .. tostring(action.humanoid or action.target) .. "' not found" end
            local frames = action.frames
            if not frames or type(frames) ~= "table" or #frames == 0 then
                return false, "❌ create_animation: 'frames' array required for keyframe type"
            end
            local anim = Instance.new("Animation")
            anim.Name = action.name or "KeyframeAnimation"
            local parent = resolveParent(action.parent, targetHum.Parent)
            anim.Parent = parent or targetHum.Parent
            local animator = targetHum:FindFirstChildOfClass("Animator")
            if not animator then animator = Instance.new("Animator"); animator.Parent = targetHum end
            -- Build keyframe sequence via a generated script
            local kfSource = "local plr = game:GetService('Players').LocalPlayer\nlocal char = plr.Character\nif not char then return end\nlocal hum = char:WaitForChild('Humanoid')\nlocal animator = hum:FindFirstChildOfClass('Animator') or Instance.new('Animator')\nif not animator.Parent then animator.Parent = hum end\nlocal anim = Instance.new('Animation')\nanim.AnimationId = 'rbxasset://animations/anim_slowrun.rbxm'\n-- keyframe animation not fully supported via JSON yet; using run animation as fallback\nlocal track = animator:LoadAnimation(anim)\ntrack:Play()\nif " .. tostring(action.loop or false) .. " then track.Looped = true end"
            local kfScript = Instance.new("LocalScript")
            kfScript.Name = (action.name or "KeyframeAnim") .. "_Loader"
            kfScript.Parent = parent or targetHum.Parent
            kfScript.Source = kfSource
            table.insert(_batchCreated, kfScript)
            updateSceneState(action, true)
            _lastClickInstance = kfScript
            return true, "🎭 Keyframe animation created (" .. #frames .. " frames)"
        else
            return false, "❌ Unknown animationType: " .. tostring(animType)
        end

    elseif kind == "create_texture" then
        local target = resolveTarget(action.target)
        if not target then return false, "❌ create_texture: target '" .. tostring(action.target) .. "' not found" end
        local texType = action.textureType or "Decal"
        local tex = Instance.new(texType)
        tex.Name = action.name or texType
        tex.Texture = action.textureId or "rbxasset://textures/trans.png"
        if action.face then tex.Face = Enum.NormalId[action.face] end
        if action.color then tex.Color3 = Color3.fromRGB(action.color[1] or 255, action.color[2] or 255, action.color[3] or 255) end
        if action.transparency then tex.Transparency = action.transparency end
        if tex:IsA("Texture") then
            if action.studsPerTileU then tex.StudsPerTileU = action.studsPerTileU end
            if action.studsPerTileV then tex.StudsPerTileV = action.studsPerTileV end
        end
        tex.Parent = target
        table.insert(_batchCreated, tex)
        updateSceneState(action, true)
        _lastClickInstance = tex
        return true, "🎨 " .. texType .. " applied to " .. target.Name

    elseif kind == "create_npc" then
        local npcName = action.name or "NPC"
        local pos = action.position
        if not pos or type(pos) ~= "table" or #pos < 3 then
            return false, "❌ create_npc requires position:[x,y,z]"
        end
        local npcColor = action.color or {180, 120, 80}
        local ws = action.walkspeed or 16
        local hp = action.health or 100
        local scale = action.resize or 1
        local npcFolder = Instance.new("Folder")
        npcFolder.Name = npcName
        npcFolder.Parent = resolveParent(action.parent, Workspace)
        -- Create NPC parts (R6 model)
        local torso = Instance.new("Part")
        torso.Name = "Torso"; torso.Size = Vector3.new(2, 2, 1); torso.Position = Vector3.new(pos[1] or 0, (pos[2] or 3) + 1.5, pos[3] or 0)
        torso.Anchored = false; torso.CanCollide = true; torso.BrickColor = BrickColor.new(Color3.fromRGB(npcColor[1] or 180, npcColor[2] or 120, npcColor[3] or 80))
        torso.Parent = npcFolder
        local head = Instance.new("Part")
        head.Name = "Head"; head.Size = Vector3.new(1, 1, 1) * scale; head.Shape = Enum.PartType.Ball; head.Position = Vector3.new(pos[1] or 0, (pos[2] or 3) + 3, pos[3] or 0)
        head.Anchored = false; head.CanCollide = true; head.BrickColor = BrickColor.new(Color3.fromRGB(255, 200, 150))
        head.Parent = npcFolder
        local larm = Instance.new("Part")
        larm.Name = "Left Arm"; larm.Size = Vector3.new(1, 2, 1); larm.Position = Vector3.new((pos[1] or 0) - 1.5, (pos[2] or 3) + 1.5, pos[3] or 0)
        larm.Anchored = false; larm.CanCollide = true; larm.BrickColor = torso.BrickColor; larm.Parent = npcFolder
        local rarm = Instance.new("Part")
        rarm.Name = "Right Arm"; rarm.Size = Vector3.new(1, 2, 1); rarm.Position = Vector3.new((pos[1] or 0) + 1.5, (pos[2] or 3) + 1.5, pos[3] or 0)
        rarm.Anchored = false; rarm.CanCollide = true; rarm.BrickColor = torso.BrickColor; rarm.Parent = npcFolder
        local lleg = Instance.new("Part")
        lleg.Name = "Left Leg"; lleg.Size = Vector3.new(1, 2, 1); lleg.Position = Vector3.new((pos[1] or 0) - 0.5, (pos[2] or 3) - 0.5, pos[3] or 0)
        lleg.Anchored = false; lleg.CanCollide = true; lleg.BrickColor = BrickColor.new(Color3.fromRGB(30, 30, 120)); lleg.Parent = npcFolder
        local rleg = Instance.new("Part")
        rleg.Name = "Right Leg"; rleg.Size = Vector3.new(1, 2, 1); rleg.Position = Vector3.new((pos[1] or 0) + 0.5, (pos[2] or 3) - 0.5, pos[3] or 0)
        rleg.Anchored = false; rleg.CanCollide = true; rleg.BrickColor = lleg.BrickColor; rleg.Parent = npcFolder
        -- Weld joints
        local function weld(p0, p1, name)
            local w = Instance.new("Weld"); w.Name = name; w.Part0 = p0; w.Part1 = p1
            w.C0 = p0.CFrame:inverse() * p1.CFrame; w.Parent = p0
        end
        weld(torso, head, "Neck"); weld(torso, larm, "Left Shoulder"); weld(torso, rarm, "Right Shoulder")
        weld(torso, lleg, "Left Hip"); weld(torso, rleg, "Right Hip")
        -- Humanoid
        local hum = Instance.new("Humanoid")
        hum.Name = "Humanoid"; hum.MaxHealth = hp; hum.Health = hp; hum.WalkSpeed = ws; hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
        hum.Parent = npcFolder
        -- HumanoidRootPart
        local root = Instance.new("Part")
        root.Name = "HumanoidRootPart"; root.Size = Vector3.new(2, 2, 1); root.Shape = Enum.PartType.Cylinder
        root.Position = Vector3.new(pos[1] or 0, (pos[2] or 3) + 0.5, pos[3] or 0)
        root.Anchored = true; root.Transparency = 1; root.CanCollide = false; root.Parent = npcFolder
        weld(torso, root, "RootJoint")
        -- Head Billboard (name tag)
        local bb = Instance.new("BillboardGui")
        bb.Name = "NameTag"; bb.Size = UDim2.new(0, 60, 0, 20); bb.StudsOffset = Vector3.new(0, 2.5, 0); bb.AlwaysOnTop = true; bb.Parent = head
        local nl = Instance.new("TextLabel")
        nl.Size = UDim2.new(1, 0, 1, 0); nl.BackgroundTransparency = 1; nl.Text = npcName; nl.TextColor3 = Color3.new(1, 1, 1)
        nl.TextSize = 14; nl.Font = Enum.Font.GothamBold; nl.TextStrokeTransparency = 0.5; nl.Parent = bb
        table.insert(_batchCreated, npcFolder)
        updateSceneState(action, true)
        _lastClickInstance = npcFolder
        return true, "🧑‍🦱 NPC created: " .. npcName .. " at " .. tostring(pos[1]) .. "," .. tostring(pos[2]) .. "," .. tostring(pos[3])

    elseif kind == "load_model" then
        local assetId = action.assetId
        if not assetId or assetId == "" then return false, "❌ load_model requires 'assetId' (rbxassetid://...)" end
        local InsertService = game:GetService("InsertService")
        local parent = resolveParent(action.parent, Workspace)
        local modelName = action.name or "LoadedModel"
        local pos = action.position
        local loaded = nil
        local ok, result = pcall(function()
            local id = assetId:match("rbxassetid://(%d+)") or assetId:match("(%d+)")
            if id then
                local m = InsertService:LoadAsset(tonumber(id))
                if m then loaded = m end
            end
        end)
        if not ok or not loaded then
            -- Fallback: create a placeholder
            local fb = Instance.new("Part")
            fb.Name = modelName .. "_Placeholder"
            fb.Size = Vector3.new(4, 4, 4)
            fb.Anchored = true; fb.CanCollide = true
            fb.Color = Color3.fromRGB(200, 100, 100)
            if pos then fb.Position = Vector3.new(pos[1] or 0, pos[2] or 5, pos[3] or 0) end
            fb.Parent = parent
            table.insert(_batchCreated, fb)
            _lastClickInstance = fb
            updateSceneState(action, true)
            return true, "⚠️ Model " .. assetId .. " couldn't load (not found or restricted). Created placeholder part instead."
        end
        loaded.Name = modelName
        if loaded.Parent then loaded.Parent = nil end
        loaded.Parent = parent
        if pos then
            local rootPart = loaded:FindFirstChild("HumanoidRootPart") or loaded:FindFirstChildWhichIsA("BasePart")
            if rootPart then rootPart.CFrame = CFrame.new(Vector3.new(pos[1] or 0, pos[2] or 5, pos[3] or 0)) end
        end
        if action.makeTool then
            loaded:FindFirstChildWhichIsA("Part").Anchored = false
        end
        table.insert(_batchCreated, loaded)
        updateSceneState(action, true)
        _lastClickInstance = loaded
        return true, "📦 Model loaded: " .. modelName .. " (" .. assetId .. ")"

    else
        return false, "❌ Unknown action type: " .. tostring(kind)
    end
end

local SYSTEM_PROMPT = [[
You are Skill-AI v1.3 — Roblox master builder. Build ANY game: tower defense, FPS, RPG, obby, racing, survival, tycoon, adventure, battle royale. NPCs, animations, textures, GUIs, plugins, guns, HP, save/load, multiplayer, leaderboards, shops, pets, waves, upgrades, boss fights — all in ONE response. Output ONLY valid JSON.

=== RULES ===
- ONLY valid JSON. No text/markdown/backticks. No reasoning. Output JSON immediately.
- NO Lua in JSON: use [255,0,0] NOT Color3.fromRGB/Color3.new. Use [5,5,5] NOT Vector3.new. Use [0.5,0,0.2,0] NOT UDim2.new.
- "className":"Part" NOT "Block"/"Ball"/"Cylinder" — use "Shape":"Box" for shape.
- Prefix [EASY]=simple(1-3 parts,minimal scripts) [NORMAL]=moderate [HARD]=full game with complete working Lua code, no placeholders, no TODOs.
- Format: {"actions":[...],"reply":"message"}. All actions in one array, executed sequentially.

=== ACTIONS (use these) ===
1. create_instance: {type,className,name,parent,containerName?,properties?,handleProperties?}
2. create_compound: {type,className:"Model",name,parent,parts:[{className,size,position,color,material,shape?}],makeTool?}
3. create_script: {type,className:"Script"|"LocalScript",name,parent,source}
4. create_multiple: {type,className,count,containerName,parent,properties?,rejectionRadius?}
5. create_enclosure: {type,name,parent,containerName?,centerX?,centerY?,centerZ?,width?,depth?,wallHeight?,floor?,openSides?,color?,material?,barrier?}
6. create_challenge_system: {type,name,parent,containerName?,challengeType,targetCount,title?,centerX?,centerY?,centerZ?,width?,depth?,height?,visibleFloor?,walls?,wallHeight?,wallColor?,wallMaterial?,guiPosition?,guiSize?}
7-14: set_property, delete, clone, rename, move, set_lighting, set_character, undo
15. create_plugin: {type,name,modules:[{name,source}],icon?,buttonColor?,parent?}
16. create_particle: {type,target,texture?,rate?,speed?,lifetime?,size?,color?,transparency?,rotation?,spread?,enabled?}
17. create_light: {type,lightType:"PointLight"|"SpotLight"|"SurfaceLight",target,color?,brightness?,range?,angle?,enabled?}
18. create_sound: {type,soundId:"rbxassetid://...",target?,parent?,name?,volume?,pitch?,loops?,startPaused?}
19. create_tween: {type,target,properties:{...},duration?,style?,direction?,repeatCount?,reverses?}
20. create_animation: {type,animationType:"keyframe"|"asset",target?,humanoid?,parent?,name?,assetId?,frames?,loop?,speed?}
21. create_texture: {type,textureType:"Decal"|"Texture",target,textureId:"rbxassetid://...",face?,color?,transparency?,studsPerTileU?,studsPerTileV?}
22. create_npc: {type,name,position,color?,walkspeed?,health?,resize?,parent?}
23. load_model: {type,assetId:"rbxassetid://...",name?,parent?,position?,makeTool?}

=== BUILD RULES ===
- PARENTING: GUI→StarterGui, Parts→Workspace, Scripts→ServerStorage/Lighting, LocalScripts→StarterPlayerScripts
- NEVER parent GUI to Workspace. NEVER parent Parts to StarterGui/StarterPlayerScripts. NEVER parent Scripts to Workspace.
- When a script controls a GUI, parent the script INSIDE the ScreenGui or to StarterPlayerScripts, NOT Workspace.
- Textures/Decals: use `target` (the part), NOT `parent`. Decal.Parent = the part, not Workspace.
- Parents: Workspace,StarterGui,StarterPlayerScripts,StarterCharacterScripts,Lighting,ServerStorage,ReplicatedStorage,CoreGui,StarterPack
- Parts: Block,Ball,Cylinder,Wedge,CornerWedge. Materials: Plastic,Metal,Wood,Glass,Neon,Granite,Marble,Cement.
- ANCHOR static=true. Only Tools=false. CanCollide=true auto. Stack Y+5 per level.
- Colors: RGB [255,0,0] or BrickColor string. Sizes: doors 4x7x1, platforms 10x0.5x10, walls 0.5x8x6.
- GUI circles: Frame+UICorner(CornerRadius=UDim.new(1,0)).
- Scripts: NEVER "workspace.X" — use :WaitForChild(). nil-guard. NEVER SourceBackup. Parts FIRST, scripts SECOND. Respawn-safe (auto re-run).
- Reply: MUST include full script source code. Numbered plan for complex builds.
- Game types: TD=path+spawn+towers+waves+currency+upgrades. FPS=guns+damage+HP+ammo+respawn. Sim=zone+collect+currency+shop+save. Race=track+vehicle+checkpoint+timer+leaderboard.
- Enclosure ex: {"type":"create_enclosure","name":"Platform","parent":"Workspace","centerX":0,"centerY":8,"centerZ":0,"width":10,"depth":10,"wallHeight":6}
- Challenge ex: {"type":"create_challenge_system","name":"KillChallenge","parent":"Workspace","challengeType":"kill","targetCount":10,"centerX":0,"centerY":0,"centerZ":0,"width":30,"depth":30,"height":8}
- HARD mode: complete working Lua in every script — every function, event, line. No placeholders or TODOs.
]]

local REASONING_PATTERNS = {
    "^Let me%s", "^I['']?ll%s", "^I think%s", "^First,?%s",
    "^The user%s", "^Okay,?%s", "^So,?%s", "^Alright,?%s",
    "^Here['']?s%s", "^This is%s", "^I need%s", "^I should%s",
    "^I will%s", "^We need%s", "^We should%s", "^We can%s",
}

local function stripReasoning(text)
    local lines = {}
    for line in text:gmatch("[^\n]+") do
        local stripped = line
        for _, pat in ipairs(REASONING_PATTERNS) do
            stripped = stripped:gsub(pat, "")
        end
        if #stripped > 0 then
            table.insert(lines, stripped)
        end
    end
    return table.concat(lines, "\n")
end

-- Fix Lua expressions inside JSON (e.g. Vector3.new(1,2,3) → [1,2,3])
local function fixLuaInJson(str)
    -- Color3.fromRGB(255,0,0) → [255,0,0]
    str = str:gsub("Color3%.fromRGB%s*%((%d+%s*,%s*%d+%s*,%s*%d+)%)", function(inner)
        return "[" .. inner .. "]"
    end)
    -- Color3.new(0.5,0,0) → [128,0,0]
    str = str:gsub("Color3%.new%s*%(([%d%.]+%s*,%s*[%d%.]+%s*,%s*[%d%.]+)%)", function(inner)
        local r, g, b = inner:match("([%d%.]+)%s*,%s*([%d%.]+)%s*,%s*([%d%.]+)")
        if r and g and b then
            r = math.floor((tonumber(r) or 0) * 255)
            g = math.floor((tonumber(g) or 0) * 255)
            b = math.floor((tonumber(b) or 0) * 255)
            return "[" .. r .. "," .. g .. "," .. b .. "]"
        end
        return "[" .. inner .. "]"
    end)
    -- Vector3.new(1,2,3) or Vector3.new(-1,2,0) → [1,2,3]
    str = str:gsub("Vector[23]%.new%s*%(([%d%.%-]+%s*,%s*[%d%.%-]+%s*,%s*[%d%.%-]+)%)", function(inner)
        return "[" .. inner .. "]"
    end)
    -- UDim2.new(0.5,0,0.2,0) → [0.5,0,0.2,0]
    str = str:gsub("UDim2%.new%s*%(([%d%.%-]+%s*,%s*[%d%.%-]+%s*,%s*[%d%.%-]+%s*,%s*[%d%.%-]+)%)", function(inner)
        return "[" .. inner .. "]"
    end)
    -- NumberRange.new(1,10) → [1,10]
    str = str:gsub("NumberRange%.new%s*%(([%d%.]+%s*,%s*[%d%.]+)%)", function(inner)
        return "[" .. inner .. "]"
    end)
    -- NumberSequence.new(1, 0.5) → {"__NumberSequence":[1,0.5]}
    str = str:gsub("NumberSequence%.new%s*%(([%d%.]+%s*,%s*[%d%.]+)%)", function(inner)
        return '{"__NumberSequence":[' .. inner .. ']}'
    end)
    -- BrickColor.new("Bright red") → "Bright red"
    str = str:gsub('BrickColor%.new%s*%("[^"]+")', function(inner)
        return inner
    end)
    -- Normalize class names
    str = str:gsub('"className"%s*:%s*"Block"', '"className":"Part"')
    str = str:gsub('"className"%s*:%s*"Ball"', '"className":"Part"')
    str = str:gsub('"className"%s*:%s*"Cylinder"', '"className":"Part"')
    -- Normalize "shape" to "Shape"
    str = str:gsub('"shape"%s*:%s*"Box"', '"Shape":"Box"')
    str = str:gsub('"shape"%s*:%s*"Block"', '"Shape":"Block"')
    str = str:gsub('"shape"%s*:%s*"Ball"', '"Shape":"Ball"')
    return str
end

local function tryExtractJson(text)
    if type(text) ~= "string" or text == "" then return nil end
    local cleaned = stripReasoning(text)
    cleaned = cleaned:gsub("```json", ""):gsub("```lua", ""):gsub("```", ""):gsub("^%s+", ""):gsub("%s+$", "")
    cleaned = cleaned:gsub("^[%s\n\r]+", ""):gsub("[%s\n\r]+$", "")
    -- strip invisible / control chars (except \t \n \r) that break JSONDecode
    cleaned = cleaned:gsub("[%c]", function(c)
        if c == "\t" or c == "\n" or c == "\r" then return c end
        return ""
    end)

    -- Convert Lua expressions inside JSON to proper JSON
    cleaned = fixLuaInJson(cleaned)

    -- If no "actions" wrapper but has a single action object, wrap it
    if not cleaned:find('"actions"', 1, true) and cleaned:match('"type"%s*:') then
        cleaned = '{"actions":[' .. cleaned .. '],"reply":"Built."}'
    end

    local function tryDecode(str)
        local ok, res = pcall(function() return HttpService:JSONDecode(str) end)
        if ok and type(res) == "table" then return res end
        return nil
    end

    -- try direct
    local plan = tryDecode(cleaned)
    if plan then return plan end

    -- try extracting from known start patterns
    for _, prefix in ipairs({'{"actions"', '{"steps"', '{"reply"', '{"type"'}) do
        local s = cleaned:find(prefix, 1, true)
        if s then
            local e = cleaned:reverse():find("}")
            if e then
                local realEnd = #cleaned - e + 1
                if realEnd > s then
                    local sub = cleaned:sub(s, realEnd)
                    plan = tryDecode(sub)
                    if plan then return plan end
                end
            end
        end
    end

    -- first { to last }
    local s = cleaned:find("{")
    local e = cleaned:reverse():find("}")
    if s and e then
        local realEnd = #cleaned - e + 1
        if realEnd > s then
            local sub = cleaned:sub(s, realEnd)
            plan = tryDecode(sub)
            if plan then return plan end
            -- maybe truncated — try adding closing braces
            local extras = { "}", "}]}", "}]}}", "} }" }
            for _, extra in ipairs(extras) do
                local fixed = sub .. extra
                plan = tryDecode(fixed)
                if plan then return plan end
            end
        end
    end

    return nil
end

local function buildSceneTree()
    local lines = {}
    local limit = 120
    local count = 0

    -- tracked containers from this session
    local hasContainers = false
    for key, ss in pairs(sceneState) do
        local names = {}
        if ss.createdNames then
            for n, _ in pairs(ss.createdNames) do
                table.insert(names, n)
            end
        end
        if #names > 0 then
            table.insert(lines, "  " .. key .. " (" .. (ss.className or "?") .. ", " .. ss.count .. " total): " .. table.concat(names, ", "))
            hasContainers = true
        end
    end
    if hasContainers then
        table.insert(lines, 1, "--- this session ---")
        table.insert(lines, 2, "")
    end

    -- live scene tree
    local servicesToScan = {
        Workspace = Workspace,
        StarterPack = StarterPack,
        Lighting = Lighting,
        ServerStorage = ServerStorage,
        ReplicatedStorage = ReplicatedStorage,
        StarterGui = StarterGui,
        StarterCharacterScripts = StarterCharacterScripts,
        StarterPlayerScripts = StarterPlayerScripts,
    }

    local function shortClass(cn)
        local map = { Part = "Part", MeshPart = "Mesh", Model = "Model", Folder = "Folder",
            Script = "Script", LocalScript = "LScript", StringValue = "StrVal",
            ScreenGui = "Gui", Frame = "Frame", TextLabel = "Label", TextButton = "Btn",
            ImageLabel = "Img", ScrollingFrame = "ScrFrame", ViewportFrame = "VpFrame" }
        return map[cn] or cn
    end

    for svcName, svc in pairs(servicesToScan) do
        if svc and type(svc) == "userdata" then
            local children = svc:GetChildren()
            if #children > 0 then
                table.insert(lines, "")
                table.insert(lines, svcName .. ":")
                for i, child in ipairs(children) do
                    if count >= limit then break end
                    local stem = (i == #children) and "    " or "│   "
                    local fork = (i == #children) and "└── " or "├── "
                    local loc = child:IsA("BasePart") and string.format(" @%.0f,%.0f,%.0f", child.CFrame.X, child.CFrame.Y, child.CFrame.Z) or ""
                    table.insert(lines, fork .. child.Name .. loc .. " (" .. shortClass(child.ClassName) .. ")")
                    count += 1
                    if (child:IsA("Model") or child:IsA("Folder")) and #child:GetChildren() > 0 then
                        local gcs = child:GetChildren()
                        for j, gc in ipairs(gcs) do
                            if count >= limit then break end
                            local gfork = (j == #gcs) and "└── " or "├── "
                            local gloc = gc:IsA("BasePart") and string.format(" @%.0f,%.0f,%.0f", gc.CFrame.X, gc.CFrame.Y, gc.CFrame.Z) or ""
                            table.insert(lines, stem .. gfork .. gc.Name .. gloc .. " (" .. shortClass(gc.ClassName) .. ")")
                            count += 1
                        end
                    end
                end
            end
        end
    end

    if #lines == 0 then return "" end
    return "=== SCENE ===\n" .. table.concat(lines, "\n") .. "\n=== END SCENE ==="
end

local _lastValidationErrors = nil

local VALIDATION_SKIP_REFS = {
    humanoid = true, handle = true, torso = true, head = true,
    ["left arm"] = true, ["right arm"] = true, ["left leg"] = true, ["right leg"] = true,
    playergui = true, backpack = true, leaderstats = true, configuration = true,
    uppertorso = true, lowertorso = true, lefthand = true, righthand = true,
    leftfoot = true, rightfoot = true,
}

local function validateActions(actions)
    local errors = {}
    local createdNames = {}
    for i, action in ipairs(actions) do
        local parent = action.parent
        if (action.type == "create_instance" or action.type == "create_compound") and action.name then
            createdNames[action.name:lower()] = true
        end
        -- Size validation: skip for GUI objects (they use UDim2 = 4-element [sX,oX,sY,oY])
        if action.type == "create_instance" and type(action.properties) == "table" then
            local guiClasses = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
                ImageLabel = true, ScrollingFrame = true, BillboardGui = true }
            local isGUI = guiClasses[action.className]
            local sz = action.properties.Size
            if type(sz) == "table" and isGUI then
                if #sz ~= 4 or type(sz[1]) ~= "number" or type(sz[2]) ~= "number" or type(sz[3]) ~= "number" or type(sz[4]) ~= "number" then
                    table.insert(errors, ("Action %d: '%s' has invalid UDim2 Size %s (needs 4 numbers: scaleX, offsetX, scaleY, offsetY)."):format(
                        i, action.name or "?", HttpService:JSONEncode(sz)
                    ))
                end
            elseif type(sz) == "table" and not isGUI then
                if not ((sz[1] or 0) > 0 and (sz[2] or 0) > 0 and (sz[3] or 0) > 0) then
                    table.insert(errors, ("Action %d: '%s' has invalid Size %s (must be positive numbers on all 3 axes)."):format(
                        i, action.name or "?", HttpService:JSONEncode(sz)
                    ))
                end
            end
            -- موقع متطرف بشكل غير منطقي (احتمال خطأ حسابي بالـ AI، مو طلب فعلي)
            local pos = action.properties.Position
            if type(pos) == "table" then
                for axisIdx = 1, 3 do
                    local v = pos[axisIdx]
                    if type(v) == "number" and math.abs(v) > 100000 then
                        table.insert(errors, ("Action %d: '%s' has extreme Position value (%d) on axis %d. Likely a calculation error — keep coordinates reasonable (e.g. within ±2000)."):format(
                            i, action.name or "?", v, axisIdx
                        ))
                    end
                end
            end
        end
        -- نفس فحص الحجم لأجزاء create_compound (كل part بالمصفوفة)
        if action.type == "create_compound" and type(action.parts) == "table" then
            for partIdx, partData in ipairs(action.parts) do
                if type(partData) == "table" and type(partData.size) == "table" then
                    local sz = partData.size
                    if not ((sz[1] or 0) > 0 and (sz[2] or 0) > 0 and (sz[3] or 0) > 0) then
                        table.insert(errors, ("Action %d (compound '%s', part %d): invalid size %s (must be positive on all 3 axes)."):format(
                            i, action.name or "?", partIdx, HttpService:JSONEncode(sz)
                        ))
                    end
                end
            end
        end
        -- GUI objects in Workspace
        if action.type == "create_instance" and parent == "Workspace" then
            local guiClasses = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
                ImageLabel = true, ScrollingFrame = true, BillboardGui = true }
            if guiClasses[action.className] then
                table.insert(errors, ("Action %d: GUI '%s' parented to Workspace. Use StarterGui instead."):format(i, action.name or "?"))
            end
        end
        -- Parts in GUI services
        if action.type == "create_instance" and parent and action.className then
            local isPart = action.className == "Part" or action.className == "MeshPart" or action.className == "WedgePart" or action.className == "CylinderPart" or action.className == "BallPart"
            local isGuiSvc = parent == "StarterGui" or parent == "StarterPlayerScripts" or parent == "StarterCharacterScripts"
            if isPart and isGuiSvc then
                table.insert(errors, ("Action %d: Part '%s' parented to %s. Use Workspace instead."):format(i, action.name or "?", parent))
            end
        end
        -- Scripts/values can't live in Workspace (not a container for code)
        if action.type == "create_script" and (parent == "Workspace") then
            table.insert(errors, ("Action %d (script '%s'): parented to Workspace. Use ServerScriptService or StarterPlayerScripts instead."):format(
                i, action.name or "?"
            ))
        end
        -- Script references a part not yet created
        if action.type == "create_script" and action.source then
            for ref in action.source:gmatch(':WaitForChild%("([^"]+)"%)') do
                local refLower = ref:lower()
                if not VALIDATION_SKIP_REFS[refLower] and not createdNames[refLower] then
                    local exists = findByName(ref)
                    if not exists then
                        table.insert(errors, ("Action %d (script '%s'): references '%s' which doesn't exist. Create it in a PRIOR action."):format(
                            i, action.name or "?", ref
                        ))
                    end
                end
            end
            for ref in action.source:gmatch(':FindFirstChild%("([^"]+)"%)') do
                local refLower = ref:lower()
                if not VALIDATION_SKIP_REFS[refLower] and not createdNames[refLower] then
                    local exists = findByName(ref)
                    if not exists then
                        table.insert(errors, ("Action %d (script '%s'): references FindFirstChild('%s') which doesn't exist. Create it first."):format(
                            i, action.name or "?", ref
                        ))
                    end
                end
            end
        end
        -- create_challenge_system validation
        if action.type == "create_challenge_system" then
            local validTypes = { reach = true, survive = true, kill = true, collect = true }
            local ct = action.challengeType
            if ct and not validTypes[ct:lower()] then
                table.insert(errors, ("Action %d (challenge '%s'): invalid challengeType '%s'. Valid: reach, survive, kill, collect"):format(
                    i, action.name or "?", ct
                ))
            end
            local tc = tonumber(action.targetCount)
            if tc and (tc < 1 or tc > 1000) then
                table.insert(errors, ("Action %d (challenge '%s'): targetCount must be 1-1000, got %s"):format(
                    i, action.name or "?", tostring(tc)
                ))
            end
            local w = tonumber(action.width)
            local d = tonumber(action.depth)
            local h = tonumber(action.height)
            if w and (w < 1 or w > 500) then
                table.insert(errors, ("Action %d (challenge '%s'): width must be 1-500, got %s"):format(i, action.name or "?", tostring(w)))
            end
            if d and (d < 1 or d > 500) then
                table.insert(errors, ("Action %d (challenge '%s'): depth must be 1-500, got %s"):format(i, action.name or "?", tostring(d)))
            end
            if h and (h < 1 or h > 200) then
                table.insert(errors, ("Action %d (challenge '%s'): height must be 1-200, got %s"):format(i, action.name or "?", tostring(h)))
            end
        end
        -- deeper script validation: detect workspace.X dot-access (forbidden by prompt rules)
        if action.type == "create_script" and action.source then
            local src = action.source
            -- pattern: workspace.Something (dot-access)
            for matchPos, _ in src:gmatch("()workspace%.%a") do
                local contextStart = math.max(1, matchPos - 15)
                local contextEnd = math.min(#src, matchPos + 25)
                local snippet = src:sub(contextStart, contextEnd):gsub("\n", "\\n")
                table.insert(errors, ("Action %d (script '%s'): uses 'workspace.X' dot-access at pos %d (\"...%s...\"). Use script.Parent:WaitForChild('X') instead."):format(
                    i, action.name or "?", matchPos, snippet
                ))
                break
            end
            -- pattern: game.Workspace.Something
            for matchPos, _ in src:gmatch("()game%.Workspace%.%a") do
                local contextStart = math.max(1, matchPos - 15)
                local contextEnd = math.min(#src, matchPos + 30)
                local snippet = src:sub(contextStart, contextEnd):gsub("\n", "\\n")
                table.insert(errors, ("Action %d (script '%s'): uses 'game.Workspace.X' dot-access at pos %d (\"...%s...\"). Use script.Parent:WaitForChild('X') instead."):format(
                    i, action.name or "?", matchPos, snippet
                ))
                break
            end
        end
    end

    -- cross-action ordering check: scripts referencing parts must come after those parts are created
    do
        local createdByAction = {}
        for i, action in ipairs(actions) do
            if (action.type == "create_instance" or action.type == "create_compound") and action.name then
                createdByAction[action.name:lower()] = i
            end
            if action.type == "create_enclosure" and action.name then
                createdByAction[action.name:lower()] = i
            end
            if action.type == "create_challenge_system" and action.name then
                createdByAction[action.name:lower()] = i
            end
            if action.type == "create_multiple" and action.namePrefix then
                createdByAction[action.namePrefix:lower()] = i
            end
        end
        for i, action in ipairs(actions) do
            if action.type == "create_script" and action.source then
                for ref in action.source:gmatch(':WaitForChild%("([^"]+)"%)') do
                    local refLower = ref:lower()
                    if not VALIDATION_SKIP_REFS[refLower] and createdByAction[refLower] and createdByAction[refLower] > i then
                        table.insert(errors, ("Action %d (script '%s'): references part '%s' via WaitForChild, but that part is created LATER in action %d. Parts must be created BEFORE scripts that reference them."):format(
                            i, action.name or "?", ref, createdByAction[refLower]
                        ))
                    end
                end
                for ref in action.source:gmatch(':FindFirstChild%("([^"]+)"%)') do
                    local refLower = ref:lower()
                    if not VALIDATION_SKIP_REFS[refLower] and createdByAction[refLower] and createdByAction[refLower] > i then
                        table.insert(errors, ("Action %d (script '%s'): references FindFirstChild('%s'), but that part is created LATER in action %d. Reorder: create first, then script."):format(
                            i, action.name or "?", ref, createdByAction[refLower]
                        ))
                    end
                end
            end
        end
    end

    -- ⚠️ فحص تنظيم: لو فيه أكثر من create_instance بدون containerName للقطع
    -- اللي مش GUI أو Script (لأن GUI عنده ScreenGui اللي هو container أصلاً)
    do
        local guiClasses = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
            ImageLabel = true, ScrollingFrame = true, BillboardGui = true, ViewportFrame = true }
        local noContainerCount = 0
        local containerNamesUsed = {}
        for _, action in ipairs(actions) do
            if action.type == "create_instance" and not guiClasses[action.className] and action.className ~= "Script" and action.className ~= "LocalScript" then
                if not action.containerName or action.containerName == "" then
                    noContainerCount = noContainerCount + 1
                else
                    containerNamesUsed[action.containerName] = (containerNamesUsed[action.containerName] or 0) + 1
                end
            end
        end
        local distinctContainers = 0
        for _ in pairs(containerNamesUsed) do distinctContainers = distinctContainers + 1 end
        if noContainerCount >= 3 then
            table.insert(errors, ("You created %d separate parts via create_instance without any containerName. Per the ORGANIZATION RULE, give them all the SAME containerName so they group under one Folder instead of scattering loose parts in the Explorer."):format(noContainerCount))
        elseif distinctContainers > 1 and noContainerCount == 0 then
            table.insert(errors, "You used multiple different containerName values for parts that appear to belong to the same build. Use ONE consistent containerName for all parts of a single logical build.")
        end
    end

    return errors
end

local function requestPlan(question, retryCount, accumUsage)
    retryCount = retryCount or 0
    accumUsage = accumUsage or { prompt_tokens = 0, completion_tokens = 0, total_tokens = 0 }
    local maxRetries = 2

    local messages = {}
    if CURRENT_PROVIDER ~= "nvidia" then
        table.insert(messages, { role = "system", content = SYSTEM_PROMPT })
    end

    -- Only send history and scene if they have content (saves tokens)
    local hasScene = next(sceneState) ~= nil
    if hasScene then
        for _, histMsg in ipairs(buildHistoryMessages()) do
            table.insert(messages, histMsg)
        end
    end

    local sceneJson = ""
    if hasScene then
        sceneJson = buildSceneTree()
    end
    local enrichedQuestion = question
    if sceneJson ~= "" then
        enrichedQuestion = question .. "\n\n[current_scene]\n" .. sceneJson .. "\n[/current_scene]"
    end

    if CURRENT_PROVIDER == "nvidia" then
        enrichedQuestion = "[System Instructions]\n" .. SYSTEM_PROMPT .. "\n\n[User Request]\n" .. enrichedQuestion
    end
    if retryCount > 0 then
        local retryMsg = "⚠️ Your last response had issues."
        if _lastValidationErrors then
            retryMsg = retryMsg .. "\nFix these errors in your actions:\n- " .. table.concat(_lastValidationErrors, "\n- ")
            _lastValidationErrors = nil
        else
            retryMsg = retryMsg .. " Send only valid JSON without extra text."
        end
        enrichedQuestion = enrichedQuestion .. "\n\n" .. retryMsg
    end
    table.insert(messages, { role = "user", content = enrichedQuestion })

    local effort = classifyEffort(question)
    local raw, err, usage = callAI(messages, nil, effort)
    if usage and type(usage) == "table" then
        accumUsage.prompt_tokens = accumUsage.prompt_tokens + (tonumber(usage.prompt_tokens) or 0)
        accumUsage.completion_tokens = accumUsage.completion_tokens + (tonumber(usage.completion_tokens) or 0)
        accumUsage.total_tokens = accumUsage.total_tokens + (tonumber(usage.total_tokens) or 0)
    end
    if not raw then
        return nil, err, accumUsage
    end

    local plan = tryExtractJson(raw)
    if not plan then
        logToFile("Impure JSON response (request: " .. question .. ")", raw)
        if retryCount < maxRetries then
            return requestPlan(question, retryCount + 1, accumUsage)
        end
        return nil, "❌ Could not parse AI plan (not valid JSON). Raw response saved to: " .. LOG_FILE, accumUsage
    end

    if plan.actions and type(plan.actions) == "table" and retryCount < maxRetries then
        local validationErrors = validateActions(plan.actions)
        if #validationErrors > 0 then
            logToFile("Validation errors (request: " .. question .. ")", table.concat(validationErrors, "\n"))
            _lastValidationErrors = validationErrors
            return requestPlan(question, retryCount + 1, accumUsage)
        end
    end
    return plan, nil, accumUsage
end

local function addLog(msg, isErr)
    if not displayArea or not displayArea.Parent then return end
    local lbl = Instance.new("TextLabel")
    lbl.Size = UDim2.new(1, -10, 0, 20)
    lbl.BackgroundTransparency = 1
    lbl.Text = msg
    lbl.TextColor3 = isErr and C.red or C.yellow
    lbl.TextSize = 10
    lbl.Font = Enum.Font.Code
    lbl.TextXAlignment = Enum.TextXAlignment.Left
    lbl.Parent = displayArea
    table.insert(_logLabels, lbl)
    task.wait()
    if displayArea and displayArea.Parent then
        displayArea.CanvasPosition = Vector2.new(0, displayArea.CanvasSize.Y.Offset)
    end
    return lbl
end

local function _runRequest(question)
    if not question or question == "" then
        _processingRequest = false
        return
    end
    _processingRequest = true
    CURRENT_REQUEST_TEXT = question
    _batchCreated = {}
    _batchPlacedGrid = nil

    task.spawn(function()
        local loading = Instance.new("TextLabel")
        local runOk, runErr = pcall(function()
            loading.Size = UDim2.new(1, -10, 0, 30)
            loading.BackgroundTransparency = 1
            local modelShort = (CURRENT_MODEL_LABEL or "?"):match("^%s*(.-)%s*$")
            if modelShort then modelShort = modelShort:sub(1,20) else modelShort = "?" end
            loading.Text = "🧠 Planning [" .. modelShort .. "] ."
            loading.TextColor3 = C.textDim
            loading.TextSize = 11
            loading.Font = Enum.Font.Gotham
            loading.Parent = displayArea

            -- Simple dot animation (no Heartbeat — saves CPU)
            task.spawn(function()
                local step = 0
                while loading and loading.Parent do
                    step = step + 1
                    local dots = string.rep(".", (step % 3) + 1)
                    loading.Text = "🧠 Planning [" .. modelShort .. "] " .. dots
                    task.wait(0.4)
                end
            end)
            local startTime = tick()
            addLog("📡 Sending to " .. CURRENT_PROVIDER:upper() .. "...")
            local plan, err, usage = requestPlan(question)
            addLog("📦 Parsing response...")
            local elapsed = tick() - startTime


            local function showCostLine()
                if usage and type(usage) == "table" and (usage.prompt_tokens or 0) + (usage.completion_tokens or 0) > 0 then
                    local pIn = usage.prompt_tokens or 0
                    local pOut = usage.completion_tokens or 0
                    -- للـ OpenCode: مجاني دائماً — السعر صفر
                    local cost = 0
                    local costStr
                    if CURRENT_PROVIDER == "nvidia" then
                        cost = (pIn / 1000000) * PRICE_PER_1M_INPUT + (pOut / 1000000) * PRICE_PER_1M_OUTPUT
                        costStr = ("💰 $%.6f"):format(cost)
                    else
                        costStr = "💚 Free"
                    end
                    addMessage(("⏱️ %.1fs | 🔤 %d in / %d out | %s"):format(elapsed, pIn, pOut, costStr), false, false, true)
                else
                    addMessage(("⏱️ %.1fs | 🔤 token usage not reported by server"):format(elapsed), false, false, true)
                end
            end

            if not plan then
                addMessage(err or "❌ Unknown error", false, true)
                pushHistory(question, "Failed to execute request (error: " .. tostring(err) .. ")")
                showCostLine()
                return
            end

            if plan.thought and type(plan.thought) == "string" and plan.thought ~= "" then
                addMessage("🧠 " .. plan.thought, false, false, true)
            end

            if plan.reply and type(plan.reply) == "string" and plan.reply ~= "" then
                addMessage("🤖 " .. plan.reply, false)
            end

            showCostLine()

            local actions = plan.actions
            if type(actions) ~= "table" or #actions == 0 then
                return
            end

            local summaryParts = {}
            local totalActions = #actions
            -- FIX v1.4: لا delay أبداً للباتشات الكبيرة — نستخدم task.wait فقط للباتشات الصغيرة (بصرياً)
            local visualDelay = totalActions > 20 and 0 or (totalActions > 8 and 0.01 or 0.04)

            addLog("⚙️ Executing " .. #actions .. " action(s)...")
            for actionIdx, action in ipairs(actions) do
                if type(action) ~= "table" or not action.type then
                    addMessage("⚠️ Skipped invalid action from AI", false, false, true)
                else
                    _lastClickInstance = nil
                    addLog("  [" .. actionIdx .. "/" .. totalActions .. "] " .. (action.type or "?") .. " → " .. (action.name or action.className or ""))
                    local successOk, didSucceed, resultMessage = pcall(executeAction, action)
                    local clickInst = _lastClickInstance
                    if successOk then
                        addMessage(resultMessage or "✅ Done", false, not didSucceed, true, clickInst)
                        if didSucceed then
                            local detail = tostring(action.type)
                            if action.className then detail = detail .. "(" .. tostring(action.className) .. ")" end
                            if action.name then detail = detail .. " name=" .. tostring(action.name) end
                            if action.namePrefix then detail = detail .. " prefix=" .. tostring(action.namePrefix) end
                            if action.containerName then detail = detail .. " container=" .. tostring(action.containerName) end
                            if action.count then detail = detail .. " count=" .. tostring(action.count) end
                            table.insert(summaryParts, detail)
                        end
                    else
                        addMessage("❌ Execution error: " .. tostring(didSucceed), false, true)
                        table.insert(summaryParts, tostring(action.type) .. " failed: " .. tostring(didSucceed))
                    end
                    if actionIdx < totalActions then
                        task.wait(visualDelay)
                    end
                end
            end

            local summary
            if #summaryParts > 0 then
                summary = "Executed: " .. table.concat(summaryParts, " | ")
            else
                summary = "No actions succeeded for this request."
            end

            if #_batchCreated > 0 then
                table.insert(undoStack, {
                    instances = _batchCreated,
                    summary = summary
                })
                while #undoStack > undoLimit do
                    table.remove(undoStack, 1)
                end
            end

            pushHistory(question, summary)
        end)

        if not runOk then
            addMessage("❌ " .. tostring(runErr), false, true)
        end
        pcall(function()
            if loading and loading.Parent then
                loading:Destroy()
            end
        end)
        pcall(function()
            for _, lbl in ipairs(_logLabels) do
                if lbl and lbl.Parent then lbl:Destroy() end
            end
            _logLabels = {}
        end)
        if #_requestQueue > 0 then
            local nextQ = table.remove(_requestQueue, 1)
            task.spawn(function() _runRequest(nextQ) end)
        else
            _processingRequest = false
        end
    end)
end

local function sendQuestion()
    local question = inputBox.Text
    if question == "" then return end

    if question == "/undo" or question == "undo" then
        inputBox.Text = ""
        if #undoStack == 0 then
            addMessage("⚠️ Nothing to undo.", false, true)
            return
        end
        local lastBatch = table.remove(undoStack)
        local removed = 0
        for _, entry in ipairs(lastBatch.instances) do
            if type(entry) == "table" and entry._markedForDestroy then
                addMessage("⚠️ Cannot restore: " .. tostring(entry._name) .. " (was already deleted)", false, false, true)
            else
                local inst = entry
                if inst and inst.Parent then
                    inst:Destroy()
                    removed += 1
                end
            end
        end
        addMessage("↩️ Undone: " .. lastBatch.summary .. " (removed " .. removed .. " items)", false, false, true)
        pushHistory("/undo", "Undid: " .. lastBatch.summary)
        return
    end

    local findPrefix = "/find "
    if question:lower():sub(1, #findPrefix) == findPrefix then
        inputBox.Text = ""
        local searchName = question:sub(#findPrefix + 1)
        if searchName == "" then
            addMessage("❌ Usage: /find Name", false, true)
            return
        end
        local results = findAllByName(searchName)
        if #results == 0 then
            addMessage("❌ No instances found containing \"" .. searchName .. "\"", false, true)
            return
        end
        local msg = "🔍 Found " .. #results .. " instance(s) for \"" .. searchName .. "\":"
        for i, res in ipairs(results) do
            msg = msg .. "\n  " .. i .. ". " .. res.Name .. " (" .. (res.ClassName or "?") .. ") → " .. getInstancePath(res)
        end
        addMessage(msg, false, false, true)
        pushHistory("/find " .. searchName, "Found " .. #results .. " instances")
        return
    end

    if question:lower() == "/diag" then
        inputBox.Text = ""
        addMessage("🧑 " .. question, true)
        -- Real diagnostic test: create a test LocalScript, parent it to Workspace,
        -- write Source, wait, read it back, and compare — raw result, no embellishment.
        local testSource = "--diag test " .. tostring(os.time())
        local testOk, testInst = pcall(function() return Instance.new("LocalScript") end)
        if not testOk or not testInst then
            addMessage("❌ Instance.new(\"LocalScript\") itself failed: " .. tostring(testInst), false, true)
            return
        end
        testInst.Name = "SkillAI_DiagTest"
        local p1ok = pcall(function() testInst.Source = testSource end)
        local r1ok, r1val = pcall(function() return testInst.Source end)
        local beforeParent = "Before parenting to game: write_ok=" .. tostring(p1ok) .. " read_ok=" .. tostring(r1ok)
            .. " match=" .. tostring(r1ok and r1val == testSource)

        testInst.Parent = Workspace
        task.wait()
        local p2ok = pcall(function() testInst.Source = testSource end)
        task.wait()
        local r2ok, r2val = pcall(function() return testInst.Source end)
        local afterParent = "After parenting to game: write_ok=" .. tostring(p2ok) .. " read_ok=" .. tostring(r2ok)
            .. " match=" .. tostring(r2ok and r2val == testSource)
            .. " length=" .. tostring(r2ok and r2val and #r2val or 0)

        local writefileAvail = "writefile available: " .. tostring(writefile ~= nil)
        local setclipboardAvail = "setclipboard available: " .. tostring(setclipboard ~= nil)

        testInst:Destroy()

        -- Additional check: verify StarterPlayerScripts/StarterCharacterScripts are
        -- the correct types (not fake Folders with the same name — the real root cause
        -- of "Not Found" even when Source writing succeeds).
        local spsClass = StarterPlayerScripts and StarterPlayerScripts.ClassName or "nil"
        local scsClass = StarterCharacterScripts and StarterCharacterScripts.ClassName or "nil"
        local spsCheck = "StarterPlayerScripts type: " .. spsClass
            .. (spsClass == "StarterPlayerScripts" and " ✅ correct" or " ❌ wrong! Expected StarterPlayerScripts, not " .. spsClass)
        local scsCheck = "StarterCharacterScripts type: " .. scsClass
            .. (scsClass == "StarterCharacterScripts" and " ✅ correct" or " ❌ wrong! Expected StarterCharacterScripts, not " .. scsClass)

        -- Real end-to-end test: create an actual LocalScript inside StarterPlayerScripts and verify we find it
        local spsTestMsg = "Skipped (StarterPlayerScripts unavailable)"
        if StarterPlayerScripts then
            local sp2ok, sp2inst = pcall(function() return Instance.new("LocalScript") end)
            if sp2ok and sp2inst then
                sp2inst.Name = "SkillAI_DiagTest2"
                sp2inst.Parent = StarterPlayerScripts
                pcall(function() sp2inst.Source = testSource end)
                task.wait()
                local foundBack = StarterPlayerScripts:FindFirstChild("SkillAI_DiagTest2")
                spsTestMsg = "Script inside StarterPlayerScripts found afterward: " .. tostring(foundBack ~= nil)
                if foundBack then foundBack:Destroy() end
            end
        end

        local finalVerdict
        if r2ok and r2val == testSource and spsClass == "StarterPlayerScripts" then
            finalVerdict = "✅ Result: Source writing works, and StarterPlayerScripts is the correct type. If you still see issues, tell me exactly what happens."
        elseif spsClass ~= "StarterPlayerScripts" then
            finalVerdict = "❌ Result: Found the root cause! StarterPlayerScripts is NOT the correct type in this session — this explains 'Not Found'. Try /diag after restarting the script with this update."
        else
            finalVerdict = "❌ Result: This Executor definitely blocks Source writing after parenting (confirmed by actual test, not guesswork)."
        end

        addMessage("🔬 Diagnostic result:\n" .. beforeParent .. "\n" .. afterParent .. "\n" .. writefileAvail .. "\n" .. setclipboardAvail
            .. "\n" .. spsCheck .. "\n" .. scsCheck .. "\n" .. spsTestMsg .. "\n\n" .. finalVerdict, false, false, true)
        pushHistory("/diag", "Ran Source-write diagnostic")
        return
    end

    if question:lower() == "/clear" then
        conversationHistory = {}
        addMessage("🧹 History cleared. AI starts fresh.", false)
        return
    end

    -- FIX: أمر /export — يصدّر تاريخ المحادثة كملف نصي أو ينسخه للـ clipboard
    if question:lower() == "/export" then
        inputBox.Text = ""
        if #conversationHistory == 0 then
            addMessage("⚠️ No conversation history to export.", false, false, true)
            return
        end
        local lines = { "=== Skill-AI v1.3 Conversation Export ===" }
        local ts = os.date and os.date("%Y-%m-%d %H:%M") or tostring(os.time())
        table.insert(lines, "Date: " .. ts)
        table.insert(lines, "Model: " .. CURRENT_MODEL_LABEL)
        table.insert(lines, "Turns: " .. #conversationHistory)
        table.insert(lines, "")
        for i, turn in ipairs(conversationHistory) do
            table.insert(lines, ("[%d] User: %s"):format(i, turn.user or ""))
            table.insert(lines, ("    AI: %s"):format(turn.summary or ""))
            table.insert(lines, "")
        end
        local exportStr = table.concat(lines, "\n")
        local saved = false
        -- محاولة حفظ ملف
        if writefile then
            local exportFile = "SkillAI_Export_" .. tostring(os.time()) .. ".txt"
            pcall(function()
                writefile(exportFile, exportStr)
                addMessage("✅ Exported " .. #conversationHistory .. " turn(s) to: " .. exportFile, false, false, true)
                saved = true
            end)
        end
        -- نسخ للـ clipboard كبديل
        if not saved and setclipboard then
            local ok = pcall(function() setclipboard(exportStr) end)
            if ok then
                addMessage("📋 Copied " .. #conversationHistory .. " turn(s) to clipboard (no writefile available)", false, false, true)
                saved = true
            end
        end
        if not saved then
            addMessage("⚠️ Export: writefile and setclipboard both unavailable in this executor.", false, false, true)
        end
        pushHistory("/export", "Exported " .. #conversationHistory .. " conversation turns")
        return
    end

    if question:lower() == "/cleanup" or question:lower() == "/clearfiles" then
        local cleaned = {}
        if writefile and delfile then
            pcall(function()
                if isfile and isfile(LOG_FILE) then
                    delfile(LOG_FILE)
                    table.insert(cleaned, LOG_FILE)
                end
            end)
            pcall(function()
                local filesToDel = {}
                if listfiles then
                    for _, f in ipairs(listfiles("/")) do
                        if type(f) == "string" and (f:find("SkillAI", 1, true) or f:find("skillai", 1, true)) then
                            table.insert(filesToDel, f)
                        end
                    end
                end
                for _, f in ipairs(filesToDel) do
                    pcall(function() delfile(f) end)
                    table.insert(cleaned, f)
                end
            end)
        end
        -- clean up test instances in workspace
        local removedCount = 0
        for _, obj in ipairs(Workspace:GetDescendants()) do
            if obj.Name:find("SkillAI_Diag", 1, true) or obj.Name:find("SkillAI_Test", 1, true) then
                pcall(function() obj:Destroy() end)
                removedCount = removedCount + 1
            end
        end
        local msg = "🧹 Cleanup done."
        if #cleaned > 0 then
            msg = msg .. " Deleted " .. #cleaned .. " file(s): " .. table.concat(cleaned, ", ")
        else
            msg = msg .. " No files to delete (writefile/delfile not available)."
        end
        if removedCount > 0 then
            msg = msg .. " Destroyed " .. removedCount .. " leftover test instance(s)."
        end
        addMessage(msg, false, false, true)
        return
    end

    inputBox.Text = ""
    addMessage("🧑 " .. question, true)
    CURRENT_REQUEST_TEXT = question

    -- request queue: if already processing, add to queue
    if _processingRequest then
        table.insert(_requestQueue, question)
        addMessage("⏳ Queued... (" .. #_requestQueue .. " waiting)", false, false, true)
        return
    end
    _processingRequest = true

    task.spawn(function() _runRequest(question) end)
end

sendBtn.MouseButton1Click:Connect(sendQuestion)

inputBox.FocusLost:Connect(function(enterPressed)
    if enterPressed then sendQuestion() end
end)

task.spawn(function()
    task.wait(1)
    pcall(setupRespawnReRunner)
    addMessage("🤖 Skill-AI v1.4\n📡 Model: " .. CURRENT_MODEL_LABEL .. "\n💡 /export لتصدير المحادثة | /clear | /undo | /find | /diag", false)
end)

print("[Skill-AI v1.4] Loaded | Timeout+Cache+SceneGC+FastRetry+HeightFix+BatchDelay fixes")

Features & Information

From: Telegram: @FreePalestine1970 full Ai model helping to your Roblox studio code And Open source code free you can do anything you want like update - upgrade you can do anything you want its open source code 🤍 and you can make him better from me Work just in Roblox studio lite It's like lemon ai but we make to Android and use Opencode ai and you need more model you need go to Nvidia and take api and open file and put it Of course it's not accurate; you need to provide a clear and complete text. It's quite normal for errors to occur; the same applies to other well-known models...
#Ai

Community Comments (3)

coniestconemanconiestconemanJul 10, 2026

did u use ai

V555V555Jul 13, 2026

ممتاز

worldigoworldigoAug 27, 2026

ai slop for ai *, gg

More Scripts for Studio Lite