Run it
Copy the script and paste it into your executor.
Players = :()
UserInputService = :()
TweenService = :()
RunService = :()
LogService = :()
CaptureService = :()
AssetService = :()
ReplicatedStorage = :()
HttpService = :()
MarketplaceService = :()
player = Players.LocalPlayer
playerGui = player:()
camera = .CurrentCamera
SERIF = .Font.GothamBold
(() SERIF = .Font.Merriweather )
C = {
bg = .(, , ),
sidebar = .(, , ),
panel = .(, , ),
hover = .(, , ),
userBubble = .(, , ),
code = .(, , ),
stroke = .(, , ),
text = .(, , ),
subtext = .(, , ),
= .(, , ),
}
MODELS = {
{ brand = , brandName = , name = , short = , id = , format = , provider = , icon = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , endpoint = , wire = , icon = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , endpoint = , wire = , icon = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , mark = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , mark = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , mark = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , mark = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , mark = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , mark = , accent = .(, , ), desc = , think = , vision = },
{ brand = , brandName = , name = , short = , id = , format = , provider = , mark = , accent = .(, , ), desc = , think = , vision = },
}
(id)
id
_, m (MODELS)
m.id == id m
(m)
p = m m.provider
p ==
p ==
(class, props, parent)
inst = .(class)
k, v (props) inst[k] = v
inst.Parent = parent
inst
(p, r) (, { CornerRadius = .(, r) }, p)
(p, color, thickness)
(, { Color = color C.stroke, Thickness = thickness ,
ApplyStrokeMode = .ApplyStrokeMode.Border }, p)
(p, t, r, b, l)
(, { PaddingTop = .(, t), PaddingRight = .(, r),
PaddingBottom = .(, b), PaddingLeft = .(, l) }, p)
MOTION =
(inst, t, props)
dur = t * MOTION
dur <=
k, v (props) inst[k] = v
TweenService:(inst, TweenInfo.(dur, .EasingStyle.Quad, .EasingDirection.Out), props):()
(props, parent)
base = {
BackgroundTransparency = , Font = .Font.Gotham, TextSize = ,
TextColor3 = C.text, TextXAlignment = .TextXAlignment.Left, TextWrapped = ,
}
k, v (props) base[k] = v
(, base, parent)
(props, parent)
base = {
BackgroundTransparency = , AutoButtonColor = , Text = ,
Font = .Font.GothamMedium, TextSize = , TextColor3 = C.text,
}
k, v (props) base[k] = v
(, base, parent)
(btn, overColor, overTransparency)
key
k, v (C)
v == overColor key = k
c0, t0, hovering = btn.BackgroundColor3, btn.BackgroundTransparency,
btn.MouseEnter:(()
hovering
hovering =
c0, t0 = btn.BackgroundColor3, btn.BackgroundTransparency
(btn, , { BackgroundColor3 = key C[key] overColor, BackgroundTransparency = overTransparency })
)
btn.MouseLeave:(()
hovering
hovering =
(btn, , { BackgroundColor3 = c0, BackgroundTransparency = t0 })
)
(value)
text = (value )
fn = setclipboard toclipboard writeclipboard
(fn) ==
(fn, text)
(parent, w, h, x, y, rot, color)
f = (, {
AnchorPoint = .(, ), Position = .(, x, , y),
Size = .(w, h), Rotation = rot, BackgroundColor3 = color, BorderSizePixel = ,
}, parent)
(f, .(w, h) / )
f
(kind, parent, color, size)
size = size
s = size /
root = (, {
BackgroundTransparency = , Size = .(size, size),
AnchorPoint = .(, ), Position = .(, ),
}, parent)
kind ==
(root, * s, * s, , * s, , color)
(root, * s, * s, - * s, - * s, , color)
(root, * s, * s, * s, - * s, -, color)
kind ==
(root, * s, * s, , , , color)
(root, * s, * s, , , -, color)
kind ==
(root, * s, * s, , , , color)
(root, * s, * s, , , , color)
kind ==
(root, * s, * s, , , , color)
kind ==
(root, * s, * s, - * s, , -, color)
(root, * s, * s, * s, , , color)
kind ==
(root, * s, * s, - * s, * s, -, color)
(root, * s, * s, * s, - * s, , color)
kind ==
(root, * s, * s, , - * s, , color)
(root, * s, * s, , , , color)
(root, * s, * s, , * s, , color)
kind ==
f = (, { AnchorPoint = .(, ), Position = .(, ),
Size = .( * s, * s), BackgroundTransparency = }, root)
(f, )
(f, color, )
(root, * s, * s, * s, - * s, , color)
(root, * s, * s, - * s, * s, -, color)
kind ==
f = (, { AnchorPoint = .(, ), Position = .(, ),
Size = .( * s, * s), BackgroundTransparency = }, root)
(f, )
(f, color, )
(root, * s, * s, - * s, - * s, -, color)
(root, * s, * s, - * s, * s, , color)
(root, * s, * s, * s, * s, , color)
kind ==
f = (, { AnchorPoint = .(, ), Position = .(, ),
Size = .( * s, * s), BackgroundTransparency = }, root)
(f, )
(f, color, )
(root, * s, * s, , , , color)
e = (, { AnchorPoint = .(, ), Position = .(, ),
Size = .( * s, * s), BackgroundTransparency = }, root)
(e, )
(e, color, )
kind ==
gear = (, { AnchorPoint = .(, ), Position = .(, ), Size = .( * s, * s), BackgroundTransparency = }, root)
(gear, )
(gear, color, )
center = (, { AnchorPoint = .(, ), Position = .(, ), Size = .( * s, * s), BackgroundColor3 = color }, root)
(center, )
i = , (root, * s, * s, , , i * , color)
kind ==
f = (, {
AnchorPoint = .(, ), Position = .(, ),
Size = .( * s, * s), BackgroundColor3 = color, BorderSizePixel = ,
}, root)
(f, )
kind ==
f = (, { AnchorPoint = .(, ), Position = .(, ),
Size = .( * s, * s), BackgroundTransparency = }, root)
(f, )
(f, color, )
(root, * s, * s, - * s, - * s, , color)
(root, * s, * s, - * s, * s, , color)
kind ==
f = (, { AnchorPoint = .(, ), Position = .(, ),
Size = .( * s, * s), BackgroundTransparency = }, root)
(f, )
(f, color, )
(root, * s, * s, - * s, - * s, , color)
kind ==
ring = (, { AnchorPoint = .(, ), Position = .(, - * s, , - * s),
Size = .( * s, * s), BackgroundTransparency = }, root)
(ring, )
(ring, color, )
(root, * s, * s, * s, * s, -, color)
kind ==
({ Size = .(, ), Text = , Font = .Font.Code,
TextSize = .(, .( * s)), TextColor3 = color,
TextXAlignment = .TextXAlignment.Center, TextYAlignment = .TextYAlignment.Center, TextWrapped = }, root)
kind ==
head = (, { AnchorPoint = .(, ), Position = .(, , , - * s),
Size = .( * s, * s), BackgroundColor3 = color, BorderSizePixel = }, root)
(head, )
body = (, { AnchorPoint = .(, ), Position = .(, , , * s),
Size = .( * s, * s), BackgroundColor3 = color, BorderSizePixel = }, root)
(body, )
root
(root, color)
_, d (root:())
d:() d.BackgroundColor3 = color
(parent, model, size)
model.mark
root = (, {
Name = , BackgroundColor3 = model.accent,
Size = .(size, size), BorderSizePixel = ,
}, parent)
(root, .(, .(size * )))
letters = (model.mark):()
({
Size = .(, ), Text = letters,
Font = .Font.GothamBold, TextSize = .(, .(size * )),
TextColor3 = .(, , ), TextXAlignment = .TextXAlignment.Center,
TextYAlignment = .TextYAlignment.Center, TextWrapped = ,
}, root)
root
(, {
Name = , BackgroundTransparency = ,
Size = .(size, size), Image = model.icon ,
ScaleType = .ScaleType.Fit, ImageColor3 = .(, , ),
}, parent)
small = camera.ViewportSize.X <
SIDEBAR_W = small .(, .(camera.ViewportSize.X * ))
conversations, current = {},
projects, currentProject = {},
modelIndex, thinkOn = ,
busy, cancelFlag, sidebarOpen = , , small
newFolder
openSettings
settings = {
console = , screen = , web = , theme = , uiScale = ,
personality = , apiKey = , opencodeApiKey = , openrouterKey = , provider = ,
autoLogo = , permission = , motion = , transcriptWidth = ,
}
API = {}
AGENTROUTER_KEY =
OPENCODE_KEY =
API.AGENTROUTER_URL =
API.AGENTROUTER_RESPONSES_URL =
API.AGENTROUTER_MESSAGES_URL =
API.OPENCODE_URL =
API.OPENCODE_INFERENCE_URL =
API.OPENROUTER_URL =
API.OPENROUTER_MODELS_URL =
API.OPENCODE_MODELS_URL =
API.AGENTROUTER_MODELS_URL =
API.OPENROUTER_KEY_URL =
API.OPENROUTER_SPOOF = { [] = , [] = }
API.(fn)
(fn) ~=
getter = getthreadidentity ( .getthreadidentity)
setter = setthreadidentity ( .setthreadidentity)
(setter) ~= ()
current =
(getter) ==
ok, value = (getter)
ok current = value
(setter, )
results = .((fn))
current ~= (setter, current)
results[] (results[], )
.(results, , results.n)
opencodeBlockedUntil =
webTainted =
testApiConnection
SETTINGS_FILE =
gameName =
placeId = .PlaceId
(()
info = MarketplaceService:(placeId)
info info.Name gameName = info.Name
)
ACTIVE_PALETTE = {}
k, v (C) ACTIVE_PALETTE[k] = v
THEMES = {
Claude = { bg = .(, , ), sidebar = .(, , ), panel = .(, , ), hover = .(, , ), userBubble = .(, , ), code = .(, , ), stroke = .(, , ), text = .(, , ), subtext = .(, , ), = .(, , ) },
ChatGPT = { bg = .(, , ), sidebar = .(, , ), panel = .(, , ), hover = .(, , ), userBubble = .(, , ), code = .(, , ), stroke = .(, , ), text = .(, , ), subtext = .(, , ), = .(, , ) },
DeepSeek = { bg = .(, , ), sidebar = .(, , ), panel = .(, , ), hover = .(, , ), userBubble = .(, , ), code = .(, , ), stroke = .(, , ), text = .(, , ), subtext = .(, , ), = .(, , ) },
GLM = { bg = .(, , ), sidebar = .(, , ), panel = .(, , ), hover = .(, , ), userBubble = .(, , ), code = .(, , ), stroke = .(, , ), text = .(, , ), subtext = .(, , ), = .(, , ) },
Kimi = { bg = .(, , ), sidebar = .(, , ), panel = .(, , ), hover = .(, , ), userBubble = .(, , ), code = .(, , ), stroke = .(, , ), text = .(, , ), subtext = .(, , ), = .(, , ) },
}
PERMISSION_MODES = {
{ id = , title = , desc = },
{ id = , title = , desc = },
{ id = , title = , desc = },
{ id = , title = , desc = , accent = },
}
(id)
_, p (PERMISSION_MODES) p.id == id p
PERMISSION_MODES[]
()
(readfile) ~= (isfile) ~=
ok, data = (()
(SETTINGS_FILE)
HttpService:((SETTINGS_FILE))
)
ok (data) ==
_, key ({ , , , , , , , , , , , , , , , , })
data[key] ~= settings[key] = data[key]
()
(writefile) ~=
ok = (() (SETTINGS_FILE, HttpService:(settings)) )
ok
()
AGENTROUTER_KEY = settings.apiKey
OPENCODE_KEY = settings.opencodeApiKey
MOTION = settings.motion == (settings.motion == )
attachments, MAX_ATTACH = {},
gui = (()
props = { Name = , ResetOnSpawn = , IgnoreGuiInset = ,
ZIndexBehavior = .ZIndexBehavior.Sibling, DisplayOrder = }
ok, built = (()
container = gethui ()
container (, props, container)
)
ok built built
(, props, playerGui)
)()
window = (, {
Name = , AnchorPoint = .(, ), Position = .(, ),
Size = .(, ), BackgroundColor3 = C.bg,
}, gui)
(window, )
(window)
windowConstraint = (, { MinSize = .(, ), MaxSize = .(, ) }, window)
winScale = (, { Scale = }, window)
main = (, {
Name = , BackgroundTransparency = , Position = .(SIDEBAR_W, ),
Size = .(, -SIDEBAR_W, , ),
}, window)
header = (, { Name = , BackgroundTransparency = , Size = .(, , , ) }, main)
loadingBar = (, { Name = , Position = .(, , , ), Size = .(, , , ), BackgroundColor3 = MODELS[].accent, BorderSizePixel = , ZIndex = }, main)
loadingBar.Visible =
loadingActive =
(active)
loadingActive = active
loadingBar.Visible = active
active
.(()
loadingActive loadingBar.Parent
loadingBar.Position = .(-, , , )
loadingBar.BackgroundColor3 = MODELS[modelIndex].accent
(loadingBar, , { Position = .(, , , ) })
.()
loadingBar.Visible =
)
menuBtn = ({ Position = .(, ), Size = .(, ),
BackgroundColor3 = C.hover }, header)
(menuBtn, )
(menuBtn, C.hover, )
(, menuBtn, C.subtext, )
titleLabel = ({
Position = .(, ), Size = .(, -, , ), Text = ,
Font = .Font.GothamMedium, TextSize = , TextWrapped = , TextTruncate = .TextTruncate.AtEnd,
}, header)
subtitleLabel = ({
Position = .(, ), Size = .(, -, , ), Text = ,
TextSize = , TextColor3 = C.subtext, TextWrapped = , TextTruncate = .TextTruncate.AtEnd,
}, header)
()
m = MODELS[modelIndex]
proj = currentProject currentProject.name
subtitleLabel.Text = proj .. .. m.brandName .. .. m.short
minBtn = ({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.hover }, header)
(minBtn, ); (minBtn, C.hover, ); (, minBtn, C.subtext, )
closeBtn = ({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.hover }, header)
(closeBtn, ); (closeBtn, C.hover, ); (, closeBtn, C.subtext, )
messageArea = (, {
Name = , Position = .(, ), Size = .(, , , -),
BackgroundTransparency = , BorderSizePixel = , ScrollBarThickness = ,
ScrollBarImageColor3 = C.stroke, CanvasSize = .(),
AutomaticCanvasSize = .AutomaticSize.Y, ScrollingDirection = .ScrollingDirection.Y,
}, main)
column = (, {
Name = , AnchorPoint = .(, ), Position = .(, ),
Size = .(, -, , ), AutomaticSize = .AutomaticSize.Y, BackgroundTransparency = ,
}, messageArea)
(, { MaxSize = .(, .huge) }, column)
(, { Padding = .(, ), SortOrder = .SortOrder.LayoutOrder }, column)
(column, , , , )
composer = (, {
Name = , AnchorPoint = .(, ), Position = .(, , , -),
Size = .(, -, , ), BackgroundColor3 = C.panel,
}, main)
(composer, )
(composer)
(, { MaxSize = .(, .huge) }, composer)
input = (, {
Name = , Position = .(, ), Size = .(, -, , ),
BackgroundTransparency = , Text = , PlaceholderText = ,
PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = , Font = .Font.Gotham,
TextXAlignment = .TextXAlignment.Left, TextYAlignment = .TextYAlignment.Top,
ClearTextOnFocus = , MultiLine = , TextWrapped = ,
}, composer)
attachRow = (, {
Name = , Position = .(, ), Size = .(, -, , ),
BackgroundTransparency = , Visible = ,
}, composer)
(, { FillDirection = .FillDirection.Horizontal, Padding = .(, ),
SortOrder = .SortOrder.LayoutOrder }, attachRow)
row = (, {
AnchorPoint = .(, ), Position = .(, , , -),
Size = .(, -, , ), BackgroundTransparency = ,
}, composer)
plusBtn = ({
AnchorPoint = .(, ), Position = .(, , , ),
Size = .(, ), BackgroundColor3 = C.panel,
}, row)
(plusBtn, )
(plusBtn, C.stroke)
(plusBtn, C.hover, )
(, plusBtn, C.subtext, )
thinkPill = ({
AnchorPoint = .(, ), Position = .(, , , ),
Size = .(, ), AutomaticSize = .AutomaticSize.X, BackgroundColor3 = C.panel,
}, row)
(thinkPill, )
thinkStroke = (thinkPill, C.stroke)
(thinkPill, , , , )
thinkDot = (, {
AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.subtext,
}, thinkPill)
(thinkDot, )
thinkLabel = ({
Size = .(, , , ), AutomaticSize = .AutomaticSize.X, Text = ,
TextSize = , Font = .Font.GothamMedium, TextColor3 = C.subtext, TextWrapped = ,
}, thinkPill)
sendBtn = ({
AnchorPoint = .(, ), Position = .(, , , ),
Size = .(, ), BackgroundColor3 = MODELS[].accent,
}, row)
(sendBtn, )
arrowIcon = (, sendBtn, .(, , ), )
stopIcon = (, sendBtn, .(, , ), )
stopIcon.Visible =
modelBtn = ({
AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.panel,
}, row)
(modelBtn, )
(modelBtn, C.stroke)
(modelBtn, C.hover, )
modelDot = (, {
AnchorPoint = .(, ), Position = .(, , , ),
Size = .(, ), BackgroundColor3 = .(, , ), BorderSizePixel = ,
}, modelBtn)
(modelDot, )
modelName = ({
Position = .(, ), Size = .(, , , ), Text = ,
TextSize = , Font = .Font.GothamMedium, TextWrapped = ,
TextTruncate = .TextTruncate.AtEnd,
}, modelBtn)
contextPct = ({
AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), Text = , TextSize = ,
TextColor3 = .(, , ), TextXAlignment = .TextXAlignment.Right, TextWrapped = ,
}, modelBtn)
chev = (, { BackgroundTransparency = , AnchorPoint = .(, ),
Position = .(, -, , ), Size = .(, ) }, modelBtn)
(, chev, C.subtext, )
moreBtn = ({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.panel }, row)
(moreBtn, )
(moreBtn, C.stroke)
(moreBtn, C.hover, )
dots = (, { BackgroundTransparency = , AnchorPoint = .(, ),
Position = .(, ), Size = .(, ) }, moreBtn)
k = ,
d = (, { AnchorPoint = .(, ), Position = .(, (k - ) * , , ),
Size = .(, ), BackgroundColor3 = C.subtext, BorderSizePixel = }, dots)
(d, )
()
m = MODELS[modelIndex]
modelName.Text = m.short
chars =
current
_, msg (current.messages)
msg.text chars += #msg.text
_, t (msg.tools {})
t.label chars += #t.label
msg.images chars += #msg.images *
pct = .(.((chars / ) / (m.context ) * + ), , )
contextPct.Text = pct ..
col = pct >= C.error (pct >= .(, , ) .(, , ))
contextPct.TextColor3 = col
modelDot.BackgroundColor3 = col
sidebar = (, {
Name = , Size = .(, SIDEBAR_W, , ), BackgroundColor3 = C.sidebar,
ClipsDescendants = , ZIndex = small ,
}, window)
(sidebar, )
(, { Size = .(, , , ), Position = .(, -, , ),
BackgroundColor3 = C.sidebar, BorderSizePixel = }, sidebar)
(, { Size = .(, , , ), Position = .(, -, , ),
BackgroundColor3 = C.stroke, BorderSizePixel = }, sidebar)
sideInner = (, { BackgroundTransparency = , Size = .(, SIDEBAR_W, , ) }, sidebar)
brandSpark = (sideInner, MODELS[], )
brandSpark.Position = .(, )
({ Position = .(, ), Size = .(, ), Text = ,
Font = SERIF, TextSize = , TextWrapped = }, sideInner)
sideCloseBtn = ({
AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.hover,
}, sideInner)
(sideCloseBtn, )
(sideCloseBtn, C.hover, )
(, sideCloseBtn, C.subtext, )
newBtn = ({
Position = .(, ), Size = .(, -, , ), BackgroundColor3 = C.panel,
}, sideInner)
(newBtn, )
(newBtn, C.stroke)
(newBtn, C.hover, )
plusHolder = (, { BackgroundTransparency = , Position = .(, ),
Size = .(, ) }, newBtn)
plusIcon = (, plusHolder, MODELS[].accent, )
({ Position = .(, ), Size = .(, -, , ), Text = ,
Font = .Font.GothamMedium, TextSize = , TextWrapped = }, newBtn)
searchBox = (, {
Position = .(, ), Size = .(, -, , ), BackgroundColor3 = C.bg,
Text = , PlaceholderText = , PlaceholderColor3 = C.subtext,
TextColor3 = C.text, TextSize = , Font = .Font.Gotham, ClearTextOnFocus = ,
TextXAlignment = .TextXAlignment.Left,
}, sideInner)
(searchBox, )
(searchBox, , , , )
convoList = (, {
Position = .(, ), Size = .(, -, , -), BackgroundTransparency = ,
BorderSizePixel = , ScrollBarThickness = , ScrollBarImageColor3 = C.stroke,
CanvasSize = .(), AutomaticCanvasSize = .AutomaticSize.Y,
}, sideInner)
(, { Padding = .(, ), SortOrder = .SortOrder.LayoutOrder }, convoList)
sideProviderLabel
userCard = (, {
Position = .(, , , -), Size = .(, -, , ), BackgroundColor3 = C.panel,
}, sideInner)
(userCard, )
avatar = (, { Position = .(, ), Size = .(, ),
BackgroundColor3 = C.hover, Image = }, userCard)
(avatar, )
.(()
ok, img = (()
Players:(player.UserId, .ThumbnailType.HeadShot, .ThumbnailSize.Size100x100)
)
ok avatar.Image = img
)
({ Position = .(, ), Size = .(, -, , ), Text = player.DisplayName,
Font = .Font.GothamMedium, TextSize = , TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, userCard)
sideProviderLabel = ({ Position = .(, ), Size = .(, -, , ),
Text = (MODELS[]),
TextSize = , TextColor3 = C.subtext, TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, userCard)
settingsBtn, layoutSidebar, workspaceNav
workspacePanel, toggleWorkspace, workspaceRefresh
nav = (, { Name = , Position = .(, ), Size = .(, -, , ),
AutomaticSize = .AutomaticSize.Y, BackgroundTransparency = }, sideInner)
(, { Padding = .(, ), SortOrder = .SortOrder.LayoutOrder }, nav)
order, subs = , {}
(label, kind, indent)
indent = indent
order = order +
row = ({ Size = .(, , , ), LayoutOrder = order }, nav)
(row, )
(row, C.hover, )
ic = (, { BackgroundTransparency = , Position = .( + indent, ),
Size = .(, ) }, row)
(kind, ic, C.subtext, )
({ Position = .( + indent, ), Size = .(, - - indent, , ),
Text = label, TextSize = , TextColor3 = C.subtext, TextWrapped = }, row)
row
(, ).MouseButton1Click:(() () )
workspaceNav = (, )
workspaceNav.MouseButton1Click:(()
toggleWorkspace ()
)
moreRow = (, )
chevIcon = (, moreRow, C.subtext, )
chevIcon.AnchorPoint = .(, )
chevIcon.Position = .(, -, , )
chevIcon.Rotation = -
(label, kind, page)
row = (label, kind, )
row.Visible =
row.MouseButton1Click:(() (page) )
.(subs, row)
(, , )
(, , )
settingsBtn = (, )
settingsBtn.MouseButton1Click:(() () )
moreOpen =
moreRow.MouseButton1Click:(()
moreOpen = moreOpen
_, r (subs) r.Visible = moreOpen
(chevIcon, , { Rotation = moreOpen - })
.(() () )
)
layoutSidebar = ()
visible =
_, ch (nav:())
ch:() ch.Visible visible = visible +
top = + visible * + .(, visible - ) +
searchBox.Position = .(, top)
convoList.Position = .(, top + )
total = sideInner.AbsoluteSize.Y
total <= total = window.AbsoluteSize.Y
total <= total =
height = total - (top + ) -
height < height =
convoList.Size = .(, -, , height)
sideInner:():(() () )
nav:():(() () )
.(() () )
toggleBtn = ({
AnchorPoint = .(, ), Position = .(, , , ),
Size = .(, ), BackgroundColor3 = C.sidebar,
}, gui)
(toggleBtn, )
(toggleBtn)
toggleSpark = (toggleBtn, MODELS[], )
toggleSpark.Position = .(, )
WS = { expanded = {}, selected = , order = , view = , source = ,
cache = { at = , items = } }
SCRIPT_CLASSES = { Script = , LocalScript = , ModuleScript = }
REMOTE_CLASSES = { RemoteEvent = , RemoteFunction = , BindableEvent = ,
BindableFunction = , UnreliableRemoteEvent = }
ROWS_CAP =
root = (, { Name = , Position = .(, ),
Size = .(, , , -), BackgroundColor3 = C.bg, Visible = , ZIndex = }, main)
workspacePanel = root
bar = (, { Size = .(, , , ), BackgroundColor3 = C.bg }, root)
(, { Position = .(, , , -), Size = .(, , , ),
BackgroundColor3 = C.stroke, BorderSizePixel = }, bar)
tabs = (, { Position = .(, ), Size = .(, ),
BackgroundTransparency = }, bar)
(, { FillDirection = .FillDirection.Horizontal, Padding = .(, ),
SortOrder = .SortOrder.LayoutOrder, VerticalAlignment = .VerticalAlignment.Center }, tabs)
chipButtons = {}
i, name ({ , , })
chip = ({ Size = .(, ), BackgroundColor3 = C.hover,
Text = name, TextSize = , LayoutOrder = i }, tabs)
(chip, )
(chip, C.hover, )
chipButtons[name] = chip
filter = (, { Position = .(, ), Size = .(, -, , ),
BackgroundColor3 = C.panel, Text = , PlaceholderText = ,
PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = ,
TextXAlignment = .TextXAlignment.Left, ClearTextOnFocus = }, bar)
(filter, )
(filter, , , , )
collapseBtn = ({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.panel }, bar)
(collapseBtn, )
(collapseBtn, C.hover, )
({ Size = .(, ), Text = , TextSize = , TextColor3 = C.subtext,
TextXAlignment = .TextXAlignment.Center, TextWrapped = }, collapseBtn)
refreshBtn = ({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.panel }, bar)
(refreshBtn, )
(refreshBtn, C.hover, )
({ Size = .(, ), Text = , TextSize = , TextColor3 = C.subtext,
TextXAlignment = .TextXAlignment.Center, TextWrapped = }, refreshBtn)
closeWs = ({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.hover }, bar)
(closeWs, )
(closeWs, C.hover, )
(, closeWs, C.subtext, )
list = (, { Position = .(, ), Size = .(, , , -),
BackgroundTransparency = , BorderSizePixel = , ScrollBarThickness = ,
ScrollBarImageColor3 = C.stroke, CanvasSize = .(),
AutomaticCanvasSize = .AutomaticSize.Y }, root)
(, { Padding = .(, ), SortOrder = .SortOrder.LayoutOrder }, list)
detail = (, { Position = .(, , , ), Size = .(, , , -),
BackgroundColor3 = C.sidebar }, root)
(, { Size = .(, , , ), BackgroundColor3 = C.stroke, BorderSizePixel = }, detail)
title = ({ Position = .(, ), Size = .(, -, , ),
Text = , Font = .Font.GothamBold, TextSize = ,
TextColor3 = C.text, TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, detail)
subtitle = ({ Position = .(, ), Size = .(, -, , ),
Text = , TextSize = , Font = .Font.Code,
TextColor3 = C.subtext, TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, detail)
propList = (, { Position = .(, ),
Size = .(, -, , -), BackgroundTransparency = , BorderSizePixel = ,
ScrollBarThickness = , ScrollBarImageColor3 = C.stroke, CanvasSize = .(),
AutomaticCanvasSize = .AutomaticSize.Y }, detail)
(, { Padding = .(, ), SortOrder = .SortOrder.LayoutOrder }, propList)
sourceScroll = (, { Position = .(, ),
Size = .(, -, , -), BackgroundColor3 = C.code, BorderSizePixel = ,
ScrollBarThickness = , ScrollBarImageColor3 = C.stroke, CanvasSize = .(),
AutomaticCanvasSize = .AutomaticSize.Y, Visible = }, detail)
(sourceScroll, )
sourceLabel = ({ Position = .(, ), Size = .(, -, , ),
AutomaticSize = .AutomaticSize.Y, Text = , Font = .Font.Code, TextSize = ,
TextColor3 = C.text, TextWrapped = }, sourceScroll)
copySource = ({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), BackgroundColor3 = C.hover, Text = , TextSize = ,
Visible = , ZIndex = }, detail)
(copySource, )
(copySource, C.panel, )
(inst)
ok, value = (() inst.ClassName )
ok (value) == value ~= value
(inst)
ok, full = (() inst:() )
ok (full) == full ~= full
(inst.Name )
(v)
t = (v)
t ==
(v)
t ==
.(, v.X, v.Y, v.Z)
t ==
.(, v.X, v.Y)
t ==
.(, .(v.R * + ), .(v.G * + ),
.(v.B * + ))
t ==
.(, v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset)
t ==
p = v.Position
.(, p.X, p.Y, p.Z)
ok, s = (, v)
ok .. t ..
#s > s = s:(, ) ..
s
(raw, current)
value = (raw )
t = (current)
t == value:() == value ==
t == (value)
t ==
x, y, z = value:()
x .((x), (y), (z))
current
t ==
x, y = value:()
x .((x), (y))
current
t ==
r, g, b = value:()
r .((r), (g), (b))
current
t ==
xs, xo, ys, yo = value:()
xs .((xs), (xo), (ys), (yo))
current
value
KNOWN_PROPS = { , , , , , , ,
, , , , , , ,
, , , , , , , ,
, , , , , }
(inst)
list, seen = {}, {}
ok, props = (() inst:() )
ok (props) ==
_, name (props)
seen[name]
seen[name] =
okGet = (() inst[name] )
okGet list[#list + ] = name
_, name (KNOWN_PROPS)
seen[name]
okGet = (() inst[name] )
okGet
seen[name] =
list[#list + ] = name
.(list)
list
()
now = .()
WS.cache.items now - WS.cache.at < WS.cache.items
items, frontier, guard = {}, { },
#frontier > #items < guard <
guard = guard +
node = .(frontier, )
ok, children = (() node:() )
ok (children) ==
_, child (children)
items[#items + ] = child
okKids, kids = (() child:() )
okKids (kids) == #kids >
frontier[#frontier + ] = child
WS.cache.items, WS.cache.at = items, now
items
(inst)
ok, value = (()
API.(() inst.Source )
)
ok , (value):(, )
(value) ~= value == ,
value
()
_, ch (propList:())
ch:() ch:()
(order, name, inst, current, editable)
rowBox = (, { Size = .(, , , ), BackgroundColor3 = C.panel,
LayoutOrder = order }, propList)
(rowBox, )
({ Position = .(, ), Size = .(, -, , ), Text = name,
TextSize = , Font = .Font.Code, TextColor3 = C.subtext,
TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, rowBox)
valueLabel = ({ Position = .(, , , ), Size = .(, -, , ),
Text = (current), TextSize = , Font = .Font.Code,
TextColor3 = C.text, TextWrapped = ,
TextTruncate = .TextTruncate.AtEnd }, rowBox)
editable
editor = (, { Position = .(, -, , ),
Size = .(, -, , ), BackgroundColor3 = C.bg, BackgroundTransparency = ,
Text = , TextSize = , Font = .Font.Code, TextColor3 = C.text,
ClearTextOnFocus = , Visible = }, rowBox)
(editor, , , , )
editBtn = ({ AnchorPoint = .(, ),
Position = .(, -, , ), Size = .(, ),
BackgroundColor3 = C.hover, Text = , TextSize = }, rowBox)
(editBtn, )
(editBtn, C.bg, )
editing =
()
editing
editing =
editor.Visible =
valueLabel.Visible =
typed = editor.Text
okSet = (() inst[name] = (typed, inst[name]) )
okSet
valueLabel.Text = (inst[name])
valueLabel.Text =
editor.FocusLost:(commit)
editBtn.MouseButton1Click:(()
editing =
editor.Text = (inst[name])
valueLabel.Visible =
editor.Visible =
editor:()
)
(inst)
WS.selected = inst
sourceScroll.Visible =
copySource.Visible =
propList.Visible =
title.Text = inst.Name .. .. (inst)
subtitle.Text = (inst)
()
order =
order = order +
(order, , inst, (inst), )
order = order +
(order, , inst, inst.Name, )
_, name ((inst))
name ~= name ~= name ~=
okGet, current = (() inst[name] )
okGet
order = order +
(order, name, inst, current, )
kids = {}
(() kids = inst:() )
order = order +
kidRow = (, { Size = .(, , , ), BackgroundTransparency = ,
LayoutOrder = order }, propList)
({ Size = .(, -, , ), Text = (#kids) .. ,
TextSize = , TextColor3 = C.subtext, TextWrapped = }, kidRow)
i, child (kids)
i >
order = order +
more = (, { Size = .(, , , ), BackgroundTransparency = ,
LayoutOrder = order }, propList)
({ Size = .(, -, , ), Text = .. (#kids - ) .. ,
TextSize = , TextColor3 = C.subtext, TextWrapped = }, more)
order = order +
childBtn = ({ Size = .(, , , ), BackgroundTransparency = ,
LayoutOrder = order }, propList)
(childBtn, )
(childBtn, C.hover, )
(childBtn, , , , )
({ Position = .(, ), Size = .(, -, , ),
Text = child.Name, TextSize = , TextColor3 = C.subtext,
TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, childBtn)
childBtn.MouseButton1Click:(()
SCRIPT_CLASSES[(child)]
(child)
(child)
)
(inst)
WS.selected = inst
propList.Visible =
sourceScroll.Visible =
copySource.Visible =
sourceScroll.CanvasPosition = .(, )
title.Text = inst.Name .. .. (inst)
subtitle.Text = (inst)
()
source, err = (inst)
source
WS.source =
copySource.Visible =
sourceLabel.TextColor3 = C.subtext
sourceLabel.Text = .. (err )
WS.source = source
sourceLabel.TextColor3 = C.text
shown = source
#shown >
shown = shown:(, ) ..
sourceLabel.Text = shown
(inst)
SCRIPT_CLASSES[(inst)]
(inst)
(inst)
(depth, inst)
children = {}
(() children = inst:() )
WS.order = WS.order +
item = ({ Size = .(, , , ), BackgroundTransparency = ,
LayoutOrder = WS.order }, list)
(item, )
(item, C.hover, )
(item, , , , + depth * )
#children >
chev = ({ Position = .(, ), Size = .(, ),
BackgroundTransparency = }, item)
arrow = (, chev, WS.selected == inst C.text C.subtext, )
arrow.Rotation = WS.expanded[inst]
chev.MouseButton1Click:(()
WS.expanded[inst] = WS.expanded[inst]
WS.render WS.()
)
dot = (, { Position = .(, ), Size = .(, ),
BackgroundColor3 = WS.selected == inst MODELS[modelIndex].accent C.stroke,
BorderSizePixel = }, item)
(dot, )
({ Position = .(, ), Size = .(, -, , ), Text = inst.Name,
TextSize = , TextColor3 = WS.selected == inst C.text C.subtext,
TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, item)
#children >
({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), Text = (#children), TextSize = ,
TextColor3 = C.subtext, TextXAlignment = .TextXAlignment.Right,
TextWrapped = }, item)
item.MouseButton1Click:(()
WS.selected = inst
#children > WS.expanded[inst]
WS.expanded[inst] =
WS.render WS.()
(inst)
)
(inst, hint)
WS.order = WS.order +
item = ({ Size = .(, , , ), BackgroundTransparency = ,
LayoutOrder = WS.order }, list)
(item, )
(item, C.hover, )
(item, , , , )
dot = (, { Position = .(, ), Size = .(, ),
BackgroundColor3 = WS.selected == inst MODELS[modelIndex].accent C.stroke,
BorderSizePixel = }, item)
(dot, )
({ Position = .(, ), Size = .(, -, , ), Text = inst.Name,
TextSize = , TextColor3 = WS.selected == inst C.text C.subtext,
TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, item)
({ Position = .(, ), Size = .(, -, , ),
Text = (inst), TextSize = , TextColor3 = C.subtext,
TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, item)
hint
({ AnchorPoint = .(, ), Position = .(, -, , ),
Size = .(, ), Text = hint, TextSize = ,
TextColor3 = C.subtext, TextXAlignment = .TextXAlignment.Right,
TextWrapped = , TextTruncate = .TextTruncate.AtEnd }, item)
item.MouseButton1Click:(()
WS.selected = inst
WS.render WS.()
(inst)
)
(value)
({ Size = .(, , , ), Text = .. value, TextSize = ,
TextColor3 = C.subtext, TextWrapped = }, list)
(inst, query)
query ==
(inst.Name):():(query, , )
(inst):():(query, , ) ~=
()
query = filter.Text:():(, ):(, )
query ~=
hits =
_, inst (())
(inst, query)
hits = hits +
hits > ROWS_CAP
(, inst)
hits ==
()
hits > ROWS_CAP
( .. (ROWS_CAP) .. )
( .. gameName)
(node, depth)
depth >
children = {}
(() children = node:() )
_, child (children)
WS.order >= ROWS_CAP
(depth, child)
WS.expanded[child] (child, depth + )
(, )
WS.order == ()
WS.order >= ROWS_CAP
( .. (ROWS_CAP) .. )
()
query = filter.Text:():(, ):(, )
hits, total = {},
_, inst (())
SCRIPT_CLASSES[(inst)]
total = total +
(inst, query) hits[#hits + ] = inst
total ==
()
.(hits, (a, b)
(a.Name):() < (b.Name):()
)
((total) .. (total == )
.. )
i, inst (hits)
i > ROWS_CAP
(inst, (inst))
#hits == ()
()
query = filter.Text:():(, ):(, )
hits, total = {},
_, inst (())
REMOTE_CLASSES[(inst)]
total = total +
(inst, query) hits[#hits + ] = inst
total ==
()
.(hits, (a, b)
(a.Name):() < (b.Name):()
)
((total) .. (total == )
.. )
i, inst (hits)
i > ROWS_CAP
(inst, (inst))
#hits == ()
()
name, chip (chipButtons)
on = WS.view == .(name)
chip.BackgroundTransparency = on
chip.TextColor3 = on C.text C.subtext
collapseBtn.Visible = WS.view ==
filter.PlaceholderText = WS.view ==
( .. WS.view)
WS.render = (keepScroll)
scrollY = list.CanvasPosition.Y
_, ch (list:())
ch:() ch:()
WS.order =
WS.view ==
()
WS.view ==
()
()
keepScroll ~= scrollY >
list.CanvasPosition = .(, scrollY)
WS.renderTree = WS.render
(name)
WS.view = name
()
root.Visible WS.()
name, chip (chipButtons)
chip.MouseButton1Click:(() (.(name)) )
collapseBtn.MouseButton1Click:(()
WS.expanded = {}
WS.render WS.()
)
workspaceRefresh = ()
root.Visible WS.render WS.()
navLabel
_, ch (workspaceNav:())
ch:() navLabel = ch
(open)
root.Visible = open
navLabel navLabel.TextColor3 = open C.text C.subtext
open
titleLabel.Text =
subtitleLabel.Text = gameName .. .. WS.view ..
()
WS.()
titleLabel.Text = current current.title
()
toggleWorkspace = ()
( root.Visible)
copySource.MouseButton1Click:(()
WS.source (WS.source)
copySource.Text =
.(, ()
copySource.Parent copySource.Text =
)
)
filter:():(()
WS.render WS.()
)
refreshBtn.MouseButton1Click:(()
WS.cache.items, WS.cache.at = ,
WS.render WS.()
)
closeWs.MouseButton1Click:(()
()
)
()
.(()
workspacePanel workspacePanel.Visible WS.()
)
readConsole
CONSOLE
CONSOLE = {
log = {}, max = , start = .(),
types = {
[.MessageType.MessageOutput] = ,
[.MessageType.MessageInfo] = ,
[.MessageType.MessageWarning] = ,
[.MessageType.MessageError] = ,
},
}
pushLog = (message, mtype, src)
.(CONSOLE.log, { t = .() - CONSOLE.start, kind = CONSOLE.types[mtype] ,
msg = (message), src = src })
#CONSOLE.log > CONSOLE.max .(CONSOLE.log, )
(()
_, e (LogService:()) (e.message, e.messageType)
)
LogService.MessageOut:((message, mtype) (message, mtype) )
(args)
args = args {}
onlyErrors = args.filter ~=
limit = .((args.limit) , , )
picked = {}
i = #CONSOLE.log, , -
e = CONSOLE.log[i]
( onlyErrors) or e.kind == "error" or e.kind == "warning" then
table.insert(picked, 1, e)
if #picked >= limit then break end
end
end
local lines, total, errors, warns = {}, 0, 0, 0
for _, e in ipairs(picked) do
if e.kind == "error" then errors += 1 elseif e.kind == "warning" then warns += 1 end
local m = e.msg
if #m > 400 then m = m:sub(1, 400) .. "...[truncated]" end
local line = string.format("[+%.1fs][%s][%s] %s", e.t, e.src, e.kind, m)
total += #line
if total > 6000 then
table.insert(lines, "...[more omitted]")
break
end
table.insert(lines, line)
end
return (#lines > 0 and table.concat(lines, "\n") or "(no matching console entries)"), errors, warns
end
local captureGame, encodePNG
do
local crcTable = {}
for i = 0, 255 do
local c = i
for _ = 1, 8 do
if bit32.band(c, 1) == 1 then c = bit32.bxor(bit32.rshift(c, 1), 0xEDB88320) else c = bit32.rshift(c, 1) end
end
crcTable[i] = c
end
local function crc32buf(b, from, to)
local c = 0xFFFFFFFF
for i = from, to do
c = bit32.bxor(crcTable[bit32.band(bit32.bxor(c, buffer.readu8(b, i)), 0xFF)], bit32.rshift(c, 8))
end
return bit32.bxor(c, 0xFFFFFFFF)
end
local function writeU32BE(b, off, v)
buffer.writeu8(b, off, bit32.band(bit32.rshift(v, 24), 255))
buffer.writeu8(b, off + 1, bit32.band(bit32.rshift(v, 16), 255))
buffer.writeu8(b, off + 2, bit32.band(bit32.rshift(v, 8), 255))
buffer.writeu8(b, off + 3, bit32.band(v, 255))
end
local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local B64c = {}
for i = 1, 64 do B64c[i - 1] = B64:sub(i, i) end
local function base64(b)
local len = buffer.len(b)
local out, n = {}, 0
for i = 0, len - 1, 3 do
local b1 = buffer.readu8(b, i)
local b2 = i + 1 < len and buffer.readu8(b, i + 1) or 0
local b3 = i + 2 < len and buffer.readu8(b, i + 2) or 0
local v = b1 * 65536 + b2 * 256 + b3
n += 1
out[n] = B64c[bit32.rshift(v, 18)] .. B64c[bit32.band(bit32.rshift(v, 12), 63)]
.. (i + 1 < len and B64c[bit32.band(bit32.rshift(v, 6), 63)] or "=")
.. (i + 2 < len and B64c[bit32.band(v, 63)] or "=")
end
return table.concat(out)
end
function encodePNG(img, targetW)
local sw, sh = img.Size.X, img.Size.Y
local tw = math.min(targetW, sw)
local th = math.max(1, math.floor(sh * tw / sw + 0.5))
local src = img:ReadPixelsBuffer(Vector2.zero, Vector2.new(sw, sh))
local stride = 1 + tw * 3
local rawLen = stride * th
local raw = buffer.create(rawLen)
for y = 0, th - 1 do
local sy = math.min(sh - 1, math.floor((y + 0.5) * sh / th))
local rowOff = y * stride
buffer.writeu8(raw, rowOff, 0)
for x = 0, tw - 1 do
local sx = math.min(sw - 1, math.floor((x + 0.5) * sw / tw))
local si = (sy * sw + sx) * 4
local di = rowOff + 1 + x * 3
buffer.writeu8(raw, di, buffer.readu8(src, si))
buffer.writeu8(raw, di + 1, buffer.readu8(src, si + 1))
buffer.writeu8(raw, di + 2, buffer.readu8(src, si + 2))
end
if y % 40 == 0 then task.wait() end
end
local a, bsum = 1, 0
for i = 0, rawLen - 1 do
a = (a + buffer.readu8(raw, i)) % 65521
bsum = (bsum + a) % 65521
end
local adler = bsum * 65536 + a
local blocks = math.ceil(rawLen / 65535)
local zLen = 2 + blocks * 5 + rawLen + 4
local out = buffer.create(8 + 25 + (12 + zLen) + 12)
local sig = { 137, 80, 78, 71, 13, 10, 26, 10 }
for i = 1, 8 do buffer.writeu8(out, i - 1, sig[i]) end
local p = 8
writeU32BE(out, p, 13); p += 4
local ihdr = p
buffer.writestring(out, p, "IHDR"); p += 4
writeU32BE(out, p, tw); p += 4
writeU32BE(out, p, th); p += 4
buffer.writeu8(out, p, 8); buffer.writeu8(out, p + 1, 2)
buffer.writeu8(out, p + 2, 0); buffer.writeu8(out, p + 3, 0); buffer.writeu8(out, p + 4, 0)
p += 5
writeU32BE(out, p, crc32buf(out, ihdr, p - 1)); p += 4
writeU32BE(out, p, zLen); p += 4
local idat = p
buffer.writestring(out, p, "IDAT"); p += 4
buffer.writeu8(out, p, 0x78); buffer.writeu8(out, p + 1, 0x01); p += 2
local pos = 0
while pos < rawLen do
local n = math.min(65535, rawLen - pos)
local nn = bit32.band(bit32.bnot(n), 0xFFFF)
buffer.writeu8(out, p, (pos + n >= rawLen) and 1 or 0)
buffer.writeu8(out, p + 1, bit32.band(n, 255)); buffer.writeu8(out, p + 2, bit32.rshift(n, 8))
buffer.writeu8(out, p + 3, bit32.band(nn, 255)); buffer.writeu8(out, p + 4, bit32.rshift(nn, 8))
p += 5
buffer.copy(out, p, raw, pos, n)
p += n
pos += n
end
writeU32BE(out, p, adler); p += 4
writeU32BE(out, p, crc32buf(out, idat, p - 1)); p += 4
writeU32BE(out, p, 0); p += 4
local iend = p
buffer.writestring(out, p, "IEND"); p += 4
writeU32BE(out, p, crc32buf(out, iend, p - 1))
return base64(out), tw, th
end
function captureGame()
local function encodedPNG(data)
if type(data) ~= "string" or #data < 64 then return nil end
if data:sub(1, 3) == "iVB" then return data end
if data:sub(2, 4) == "PNG" then return base64(buffer.fromstring(data)) end
return nil
end
local names = { "getScreenshot", "takeScreenshot", "screenshot", "captureScreenshot", "getScreenCapture" }
local function executorScreenshot()
local okEnv, env = pcall(function() return (getgenv and getgenv()) or _G end)
if not okEnv or type(env) ~= "table" then return nil end
for _, name in ipairs(names) do
local okFn, fn = pcall(function() return env[name] end)
if okFn and type(fn) == "function" then
local okOut, out = pcall(fn)
local encoded = okOut and encodedPNG(out)
if encoded then return encoded end
if okOut and type(out) == "string" and type(readfile) == "function" then
local okFile, data = pcall(readfile, out)
if okFile then
encoded = encodedPNG(data)
if encoded then return encoded end
end
end
end
end
return nil
end
gui.Enabled = false
task.wait(0.15)
local quick = executorScreenshot()
if quick then
gui.Enabled = true
return { base64 = quick, mediaType = "image/png" }
end
local contentId, done = nil, false
local okCap, errCap = pcall(function()
CaptureService:CaptureScreenshot(function(id)
contentId, done = id, true
end)
end)
local t0 = os.clock()
while okCap and not done and os.clock() - t0 < 5 do task.wait() end
gui.Enabled = true
if not okCap then return nil, tostring(errCap) end
if not contentId then return nil, "screenshot timed out" end
local okImg, img = pcall(function()
return AssetService:CreateEditableImageAsync(Content.fromUri(contentId))
end)
if not okImg then return nil, tostring(img) end
return img
end
end
local SCREENSHOT_WIDTH = 512
function captureAttachment()
local img, err = captureGame()
if not img then return nil, err end
if img.base64 then return img end
local ok, b64, w, h = pcall(encodePNG, img, SCREENSHOT_WIDTH)
if not ok then return nil, tostring(b64) end
return { editable = img, base64 = b64, mediaType = "image/png", w = w, h = h }
end
local TOOL_SCHEMAS = {
{
name = "read_console",
access = "read",
description = "Read recent Roblox console output (errors and warnings) from the running game. "
.. "Use RARELY: only when the user reports a bug, crash or unexpected behavior, or when runtime output is "
.. "truly needed to diagnose. Never for general questions. At most once per turn unless clearly insufficient.",
parameters = { filter = "'errors' (default) or 'all'", limit = "max entries, default 40" },
setting = "console",
},
{
name = "take_screenshot",
access = "read",
description = "Capture what the player currently sees in the game. Use only when visual state matters "
.. "(UI layout, rendering problems). Rarely needed.",
parameters = {},
setting = "screen",
},
{
name = "inspect_player",
access = "read",
description = "Look at the player and the live game. view='avatar' (default) returns their character: rig type, "
.. "body scales and colours, every accessory and clothing item, equipped tools, and attaches a picture "
.. "of the avatar. view='scene' returns the place, the players in the server and their distance, "
.. "your position, the camera, the time of day and the on-screen interfaces. Use this whenever the user "
.. "asks about their avatar, outfit, appearance, character, health, position, or what is around them.",
parameters = { view = "'avatar' (default) or 'scene'" },
},
{
name = "inspect_instance",
access = "read",
description = "Inspect an instance in the game by path (e.g. 'game.Workspace' or 'Players.LocalPlayer'). "
.. "Returns its class, key properties and children. Read only.",
parameters = { path = "dotted path to the instance" },
},
{
name = "read_script",
access = "read",
description = "Read the Source of a Script/LocalScript/ModuleScript by path. Returns the Luau source. "
.. "May be locked for some scripts.",
parameters = { path = "dotted path to the script" },
},
{
name = "read_file",
access = "read",
description = "Read a text file from your executor workspace by path.",
parameters = { path = "file path" },
},
{
name = "web_search",
access = "read",
description = "Search the web and return the top results with titles, URLs and snippets. "
.. "Use for current information, documentation, versions, prices, or anything you are unsure of. "
.. "When your answer relies on results, cite sources as markdown links: [title](https://example.com/page).",
parameters = { query = "search query" },
setting = "web",
},
{
name = "web_fetch",
access = "read",
description = "Fetch a web page by URL and return its readable text content. "
.. "Use to read in full a source found via web_search or a URL the user gave you.",
parameters = { url = "http(s) URL to read" },
setting = "web",
},
{
name = "set_property",
access = "write",
description = "Change a property on a game instance (e.g. Anchored, Transparency, Position). Modifies the live game.",
parameters = { path = "dotted path to the instance", property = "property name", value = "new value" },
},
{
name = "write_file",
access = "write",
description = "Write a text file to your executor workspace.",
parameters = { path = "file path", content = "file contents" },
},
{
name = "execute_luau",
access = "execute",
description = "Execute Luau code against the live game client and return printed output or errors. This runs real code.",
parameters = { code = "Luau source to run" },
},
}
function toolAccess(name)
for _, t in ipairs(TOOL_SCHEMAS) do if t.name == name then return t.access or "read" end end
return "read"
end
local SENSITIVE_FILES = { ["agentrouterrobloxai.json"] = true, [".env"] = true }
local function isSensitivePath(path)
local p = tostring(path or ""):lower():gsub("\\", "/")
return SENSITIVE_FILES[p:match("([^/]+)$") or p] == true
end
local function redactSecrets(text)
local s = tostring(text or "")
for _, key in ipairs({ settings.apiKey, settings.opencodeApiKey, settings.openrouterKey, AGENTROUTER_KEY, OPENCODE_KEY }) do
local k = tostring(key or ""):match("^%s*(.-)%s*$")
if #k >= 12 then
s = s:gsub(k:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1"), "[redacted]")
end
end
return s
end
function permissionDecision(access)
local p = settings.permission
if p == "readonly" then
return access == "read" and "run" or "deny"
end
-- A page the model just read is untrusted input. Nothing that changes state runs
-- without an explicit yes for the rest of that turn.
if access ~= "read" and webTainted then
return "confirm"
end
if p == "ask" then
return access == "read" and "run" or "confirm"
elseif p == "auto" then
return access == "execute" and "confirm" or "run"
end
return "run"
end
function findDeep(name, root, depth)
depth = depth or 0
if depth > 7 then return nil end
local ok, kids = pcall(function() return (root or workspace):GetChildren() end)
if not ok or not kids then return nil end
for _, ch in ipairs(kids) do
if ch.Name:lower() == name:lower() then return ch end
local f = findDeep(name, ch, depth + 1)
if f then return f end
end
return nil
end
local function resolveInstance(path)
if type(path) ~= "string" or path == "" then return nil end
local cleaned = path:gsub(":", ".")
local node = game
for seg in cleaned:gmatch("[^%.]+") do
if node == nil then return nil end
if node == game and seg:lower() == "game" then
node = game
elseif node == game and seg:lower() == "workspace" then
node = workspace
else
local nxt = nil
if node == game then
local ok, svc = pcall(function() return game:GetService(seg) end)
if ok and svc ~= nil then nxt = svc end
end
if nxt == nil then
local ok, ch = pcall(function() return node:FindFirstChild(seg) end)
if ok then nxt = ch end
end
node = nxt
end
end
if node == nil or node == game then node = findDeep((cleaned:gmatch("[^%.]+")() or cleaned)) or node end
if node == game then return nil end
return node
end
function confirmAction(toolName, access)
local resolved, decision = false, false
local overlay = new("Frame", { Name = "ConfirmOverlay", Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 0.5, ZIndex = 200 }, gui)
local box = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(360, 176), BackgroundColor3 = C.panel, ZIndex = 201 }, overlay)
round(box, 14)
stroke(box)
text({ Position = UDim2.fromOffset(18, 16), Size = UDim2.new(1, -36, 0, 24), Text = "Allow this action?", Font = Enum.Font.GothamBold, TextSize = 17, ZIndex = 202 }, box)
text({ Position = UDim2.fromOffset(18, 48), Size = UDim2.new(1, -36, 0, 56), Text = tostring(toolName) .. "\nPermission level: " .. tostring(access) .. " · skips in 30s", TextSize = 13, TextColor3 = C.subtext, TextWrapped = true, ZIndex = 202 }, box)
local denyBtn = button({ Position = UDim2.new(0, 18, 1, -54), Size = UDim2.new(0.5, -27, 0, 36), BackgroundColor3 = C.hover, Text = "Deny", TextSize = 14, ZIndex = 202 }, box)
round(denyBtn, 9)
local allowBtn = button({ Position = UDim2.new(0.5, 9, 1, -54), Size = UDim2.new(0.5, -27, 0, 36), BackgroundColor3 = C.error, Text = "Allow", TextSize = 14, ZIndex = 202 }, box)
round(allowBtn, 9)
local countdown = text({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -18, 0, 14),
Size = UDim2.fromOffset(48, 20), Text = "30s", TextSize = 12, TextColor3 = C.subtext,
TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false, ZIndex = 202 }, box)
denyBtn.MouseButton1Click:Connect(function() decision = false resolved = true end)
allowBtn.MouseButton1Click:Connect(function() decision = true resolved = true end)
local t0 = os.clock()
while not resolved and overlay.Parent and os.clock() - t0 < 30 do
countdown.Text = tostring(math.max(0, math.ceil(30 - (os.clock() - t0)))) .. "s"
task.wait(0.05)
end
overlay:Destroy()
if not resolved then return "timeout" end
return decision and "allow" or "deny"
end
function activeToolSchemas()
local list = {}
for _, t in ipairs(TOOL_SCHEMAS) do
local shown = true
if t.setting ~= nil then shown = settings[t.setting] ~= false end
if shown and not (settings.permission == "readonly" and (t.access or "read") ~= "read") then
table.insert(list, t)
end
end
return list
end
local webSearch, webFetch
function runTool(call, approved)
local access = toolAccess(call.name)
if not approved then
local decision = permissionDecision(access)
if decision == "deny" then
return "Blocked by the Read only permission mode.", "Blocked (read only)", "denied"
end
if decision == "confirm" then
local answer = confirmAction(call.name, access)
if answer == "timeout" then
return "The approval prompt timed out after 30 seconds, so the action was skipped.", "Timed out", "denied"
elseif answer ~= "allow" then
return "The user declined this action.", "Declined", "denied"
end
end
end
if call.name == "read_console" then
if not settings.console then return "Console access is turned off by the user.", "Console access is off", "denied" end
local out, e, w = readConsole(call.args)
return out, string.format("Read console: %d error%s, %d warning%s",
e, e == 1 and "" or "s", w, w == 1 and "" or "s"), "done"
elseif call.name == "take_screenshot" then
if not settings.screen then return "Game screenshots are turned off by the user.", "Game vision is off", "denied" end
local att, err = captureAttachment()
if not att then
return "Screenshot failed: " .. tostring(err)
.. "\nThis executor blocks screen capture, so no image is available. Use inspect_player instead "
.. "(it reads the avatar and the scene and can attach the avatar picture), or ask the user to "
.. "attach a screenshot in the composer.", "Screenshot blocked", "error"
end
return "Screenshot attached.", "Took a screenshot", "done", att
elseif call.name == "inspect_player" then
local view = tostring((call.args and call.args.view) or "avatar"):lower()
if view ~= "scene" then view = "avatar" end
local report = API.playerReport(view)
if view == "avatar" then
local image = API.avatarImage()
if image then return report .. "\nAvatar picture attached.", "Looked at your avatar", "done", image end
end
return report, view == "scene" and "Looked around the game" or "Read your avatar", "done"
elseif call.name == "inspect_instance" then
local target = resolveInstance((call.args and call.args.path) or "")
if not target then return "Instance not found.", "Not found", "error" end
local lines = { target.ClassName .. " " .. target:GetFullName() }
for _, p in ipairs({ "Position", "Size", "Transparency", "Anchored", "Enabled", "Value", "ClassName" }) do
local ok, v = pcall(function() return target[p] end)
if ok and v ~= nil and type(v) ~= "userdata" then lines[#lines + 1] = p .. " = " .. tostring(v) end
if ok and typeof(v) == "Vector3" then lines[#lines + 1] = p .. " = " .. tostring(v) end
end
local kids = {}
pcall(function() kids = target:GetChildren() end)
lines[#lines + 1] = "Children (" .. #kids .. "):"
for i, ch in ipairs(kids) do
if i > 40 then lines[#lines + 1] = " ... +" .. (#kids - 40) .. " more"; break end
lines[#lines + 1] = " " .. ch.Name .. " (" .. ch.ClassName .. ")"
end
return table.concat(lines, "\n"), "Inspected " .. target.Name, "done"
elseif call.name == "read_script" then
local target = resolveInstance((call.args and call.args.path) or "")
if not target then return "Script not found.", "Not found", "error" end
local ok, src = API.withIdentity(function() return pcall(function() return target.Source end) end)
if not ok or type(src) ~= "string" then return "Could not read Source (locked or not a script).", "Read blocked", "error" end
return src, "Read " .. target.Name, "done"
elseif call.name == "read_file" then
local path = tostring((call.args and call.args.path) or "")
if path == "" then return "No path given.", "No path", "error" end
if isSensitivePath(path) then return "That file holds credentials and is not readable by tools.", "Blocked (credentials)", "denied" end
if type(readfile) ~= "function" then return "File API unavailable in this executor.", "No file API", "error" end
local ok, data = pcall(function() return readfile(path) end)
if not ok then return "File not found or unreadable: " .. path, "Read failed", "error" end
return tostring(data), "Read " .. path, "done"
elseif call.name == "web_search" then
if not settings.web then return "Web search is turned off by the user.", "Web search is off", "denied" end
local q = tostring((call.args and call.args.query) or ""):gsub("^%s+", ""):gsub("%s+$", "")
if q == "" then return "No search query given.", "No query", "error" end
local out, err = webSearch(q)
if not out then return "Search failed: " .. tostring(err), "Search failed", "error" end
webTainted = true
return out, "Searched the web: " .. (#q > 36 and q:sub(1, 33) .. "..." or q), "done"
elseif call.name == "web_fetch" then
if not settings.web then return "Web access is turned off by the user.", "Web access is off", "denied" end
local url = tostring((call.args and call.args.url) or ""):gsub("^%s+", ""):gsub("%s+$", "")
if url == "" then return "No URL given.", "No URL", "error" end
local out, err = webFetch(url)
if not out then return "Could not read the page: " .. tostring(err), "Fetch failed", "error" end
webTainted = true
return out, "Read " .. (url:match("^https?://([^/]+)") or url), "done"
elseif call.name == "set_property" then
local args = call.args or {}
local target = resolveInstance(args.path or "")
if not target then return "Instance not found.", "Not found", "error" end
local prop = tostring(args.property or "")
if prop == "" then return "No property named.", "No property", "error" end
local val = args.value
local okGet, cur = pcall(function() return target[prop] end)
if okGet and typeof(cur) == "Vector3" and type(val) == "table" then
val = Vector3.new(val[1] or val.x or 0, val[2] or val.y or 0, val[3] or val.z or 0)
end
local ok, err = API.withIdentity(function() return pcall(function() target[prop] = val end) end)
if not ok then return "Failed to set " .. prop .. ": " .. tostring(err), "Set failed", "error" end
return "Set " .. prop .. " = " .. tostring(val), "Set " .. prop, "done"
elseif call.name == "write_file" then
local args = call.args or {}
local path = tostring(args.path or "")
if path == "" then return "No path given.", "No path", "error" end
if isSensitivePath(path) then return "That file holds credentials and is not writable by tools.", "Blocked (credentials)", "denied" end
if type(writefile) ~= "function" then return "File API unavailable in this executor.", "No file API", "error" end
local content = tostring(args.content or "")
local ok, err = pcall(function() writefile(path, content) end)
if not ok then return "Write failed: " .. tostring(err), "Write failed", "error" end
return "Wrote " .. #content .. " bytes to " .. path, "Wrote " .. path, "done"
elseif call.name == "execute_luau" then
local code = tostring((call.args and call.args.code) or "")
if code == "" then return "No code provided.", "No code", "error" end
if type(loadstring) ~= "function" then
return "This executor does not provide loadstring, so code cannot run here.", "No loadstring", "error"
end
local out = {}
local function push(...)
local t = {}
for i = 1, select("#", ...) do t[#t + 1] = tostring(select(i, ...)) end
out[#out + 1] = table.concat(t, " ")
end
local okLoad, fn, cerr = pcall(loadstring, code)
if not okLoad then return "Compile error: " .. tostring(fn), "Compile error", "error" end
if not fn then return "Compile error: " .. tostring(cerr), "Compile error", "error" end
local env = setmetatable({ print = push, warn = push, write = push }, { __index = _G })
if type(setfenv) == "function" then pcall(setfenv, fn, env) end
local okRun, rerr = API.withIdentity(function() return pcall(fn) end)
local output = table.concat(out, "\n")
if not okRun then
local msg = "Runtime error: " .. tostring(rerr)
if output ~= "" then msg = output .. "\n" .. msg end
return msg, "Execution error", "error"
end
return output ~= "" and output or "Executed without output.", "Executed Luau", "done"
end
if call.name == "bash" or call.name == "edit" or call.name == "glob" or call.name == "grep" or call.name == "read" then
return "Shell and editor tools are not available here. Use inspect_instance, read_script, read_file, write_file, set_property or execute_luau instead.", "Tool unavailable", "denied"
end
return "Unknown tool.", "Unknown tool: " .. tostring(call.name), "error"
end
function parseSegments(src)
local segs, pos = {}, 1
while true do
local s, e = src:find("```", pos, true)
if not s then
table.insert(segs, { kind = "text", text = src:sub(pos) })
break
end
table.insert(segs, { kind = "text", text = src:sub(pos, s - 1) })
local close, closeE = src:find("```", e + 1, true)
local raw
if close then
raw = src:sub(e + 1, close - 1)
pos = closeE + 1
else
raw = src:sub(e + 1)
pos = #src + 1
end
local lang, body = raw:match("^([%w_%-]*)\n(.*)$")
if not lang then lang, body = "", raw end
table.insert(segs, { kind = "code", lang = lang, text = (body:gsub("\n$", "")) })
if not close then break end
end
return segs
end
codeHex = Color3.fromRGB(240, 165, 135):ToHex()
linkHex = Color3.fromRGB(96, 146, 245):ToHex()
function richify(s)
s = s:gsub("&", "&"):gsub("<", "<"):gsub(">", ">")
s = s:gsub("`([^`\n]+)`", '<font color="#' .. codeHex .. '">%1</font>')
s = s:gsub("%*%*(.-)%*%*", "<b>%1</b>")
s = s:gsub("%[([^%[%]]+)%]%((https?://[^%s%)]+)%)", '<u><font color="#' .. linkHex .. '">%1</font></u>')
s = s:gsub("https?://[%w%-%._~:/%?#%[%]@!$&'()*+,;=%%]+", function(u)
local clean = u:gsub("[%.%,%;%!%)%]]+$", "")
return '<u><font color="#' .. linkHex .. '">' .. clean .. "</font></u>" .. u:sub(#clean + 1)
end)
local lines = {}
for line in (s .. "\n"):gmatch("(.-)\n") do
line = line:gsub("^#+%s+(.*)$", "<b>%1</b>")
line = line:gsub("^%s*[%-%*]%s+", "• ")
table.insert(lines, line)
end
return table.concat(lines, "\n")
end
function renderContent(container, str)
for _, ch in ipairs(container:GetChildren()) do
if ch:IsA("GuiObject") then ch:Destroy() end
end
local order = 0
for _, seg in ipairs(parseSegments(str)) do
if seg.kind == "text" then
local t = seg.text:match("^%s*(.-)%s*$")
if t ~= "" then
order += 1
text({
Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, RichText = true,
Text = richify(t), TextSize = 15, LineHeight = 1.3, LayoutOrder = order,
}, container)
end
else
order += 1
local lineCount = 1
for _ in string.gmatch(seg.text, "\n") do lineCount = lineCount + 1 end
if lineCount > 50 then
local fileBtn = button({ Size = UDim2.new(1, 0, 0, 52), BackgroundColor3 = C.panel, LayoutOrder = order }, container)
round(fileBtn, 10)
stroke(fileBtn)
hover(fileBtn, C.hover, 0)
local iconHolder = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(12, 12), Size = UDim2.fromOffset(28, 28) }, fileBtn)
icon("file", iconHolder, C.text, 24)
text({ Position = UDim2.fromOffset(52, 8), Size = UDim2.new(1, -64, 0, 20), Text = (seg.lang ~= "" and seg.lang or "txt") .. " file", Font = Enum.Font.GothamMedium, TextSize = 14, TextWrapped = false }, fileBtn)
text({ Position = UDim2.fromOffset(52, 26), Size = UDim2.new(1, -64, 0, 16), Text = tostring(lineCount) .. " lines · Click to view", TextSize = 12, TextColor3 = C.subtext, TextWrapped = false }, fileBtn)
fileBtn.MouseButton1Click:Connect(function()
copyToClipboard(seg.text)
end)
else
local box = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundColor3 = C.code, LayoutOrder = order }, container)
round(box, 10)
stroke(box)
new("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder }, box)
local head = new("Frame", { Size = UDim2.new(1, 0, 0, 34), BackgroundTransparency = 1, LayoutOrder = 1 }, box)
local dot = new("Frame", { Position = UDim2.fromOffset(14, 13), Size = UDim2.fromOffset(8, 8),
BackgroundColor3 = Color3.fromRGB(240, 165, 135), BorderSizePixel = 0 }, head)
round(dot, 4)
local langName = (seg.lang ~= "" and seg.lang or "code"):lower()
text({ Position = UDim2.fromOffset(29, 0), Size = UDim2.new(1, -150, 1, 0),
Text = langName, TextSize = 11, TextColor3 = C.subtext,
Font = Enum.Font.GothamBold, TextWrapped = false }, head)
local copyBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0),
Size = UDim2.fromOffset(56, 24), BackgroundColor3 = C.hover, Text = "Copy", TextSize = 11 }, head)
round(copyBtn, 6)
hover(copyBtn, C.panel, 0)
copyBtn.MouseButton1Click:Connect(function()
if copyToClipboard(seg.text) then
copyBtn.Text = "Copied"
task.delay(1.2, function()
if copyBtn.Parent then copyBtn.Text = "Copy" end
end)
end
end)
new("Frame", { Position = UDim2.new(0, 0, 1, -1), Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = C.stroke, BorderSizePixel = 0 }, head)
local body = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 2 }, box)
pad(body, 10, 14, 14, 14)
text({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, Text = seg.text,
Font = Enum.Font.Code, TextSize = 13, TextColor3 = Color3.fromRGB(232, 228, 218) }, body)
end
end
end
end
function scrollToBottom()
task.defer(function()
RunService.Heartbeat:Wait()
messageArea.CanvasPosition = Vector2.new(0, math.max(0, messageArea.AbsoluteCanvasSize.Y - messageArea.AbsoluteSize.Y))
end)
end
local updateComposer, generate, send, refreshSidebar, refreshAttachStrip, attachScreenshot, refreshProjects
function drawToolChip(parent, tl, order)
local chip = new("Frame", { Size = UDim2.fromOffset(0, 28), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.panel, LayoutOrder = order }, parent)
round(chip, 14)
stroke(chip)
pad(chip, 0, 12, 0, 0)
local kindMap = { web = "globe", web_search = "globe", web_fetch = "globe", note = "check", read_console = "terminal", take_screenshot = "image",
inspect_instance = "search", read_script = "file", read_file = "file", write_file = "file",
set_property = "settings", execute_luau = "terminal", task = "terminal" }
local kind = kindMap[tl.kind] or "terminal"
local col = tl.state == "error" and C.error or C.subtext
local holder = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(9, 6),
Size = UDim2.fromOffset(16, 16) }, chip)
icon(kind, holder, col, 16)
local label = tl.label
if #label > 64 then label = label:sub(1, 61) .. "..." end
if tl.state == "running" then label = label .. "..." end
text({ Position = UDim2.fromOffset(32, 0), Size = UDim2.new(0, 0, 1, 0), AutomaticSize = Enum.AutomaticSize.X,
Text = label, TextSize = 12, TextColor3 = col, TextWrapped = false }, chip)
end
function drawUser(msg, index)
local r = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = index }, column)
new("UIListLayout", { HorizontalAlignment = Enum.HorizontalAlignment.Right, Padding = UDim.new(0, 6),
SortOrder = Enum.SortOrder.LayoutOrder }, r)
text({ Size = UDim2.fromOffset(0, 16), AutomaticSize = Enum.AutomaticSize.X, Text = player.DisplayName,
TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 0 }, r)
if msg.images and #msg.images > 0 then
local imgs = new("Frame", { Size = UDim2.fromOffset(0, 68), AutomaticSize = Enum.AutomaticSize.X,
BackgroundTransparency = 1, LayoutOrder = 1 }, r)
new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6),
SortOrder = Enum.SortOrder.LayoutOrder }, imgs)
for i, att in ipairs(msg.images) do
local il = new("ImageLabel", { Size = UDim2.fromOffset(120, 68), BackgroundColor3 = C.userBubble,
ScaleType = Enum.ScaleType.Crop, LayoutOrder = i }, imgs)
round(il, 10)
stroke(il)
pcall(function() il.ImageContent = Content.fromObject(att.editable) end)
end
end
if msg.text ~= "" then
local bubble = new("Frame", { AutomaticSize = Enum.AutomaticSize.XY, Size = UDim2.fromOffset(0, 0),
BackgroundColor3 = C.userBubble, LayoutOrder = 2 }, r)
round(bubble, 16)
stroke(bubble, C.stroke)
pad(bubble, 10, 16, 10, 16)
local colW = column.AbsoluteSize.X
local lbl = text({ AutomaticSize = Enum.AutomaticSize.XY, Size = UDim2.fromOffset(0, 0),
Text = msg.text, TextSize = 15, LineHeight = 1.25 }, bubble)
new("UISizeConstraint", { MaxSize = Vector2.new(colW > 0 and math.min(480, colW * 0.8) or 420, math.huge) }, lbl)
end
end
function formatTime(seconds)
if seconds >= 60 then
local mins = math.floor(seconds / 60)
local secs = seconds % 60
return string.format("%dm %02ds", mins, secs)
else
return string.format("%ds", seconds)
end
end
function extractSources(str)
local seen, list = {}, {}
local function add(url, title)
url = url:gsub("[%.%,%;%!%)%]]+$", "")
if url:match("^https?://") and not seen[url] and #list < 10 then
seen[url] = true
list[#list + 1] = { url = url, title = title or (url:match("^https?://([^/]+)") or url) }
end
end
for title, url in str:gmatch("%[([^%[%]]+)%]%((https?://[^%s%)]+)%)") do
add(url, (title:gsub("^%s+", ""):gsub("%s+$", "")))
end
for url in str:gmatch("https?://[%w%-%._~:/%?#%[%]@!$&'()*+,;=%%]+") do
add(url)
end
return list
end
function drawSourceRow(parent, src, order)
local row = button({ Size = UDim2.new(1, 0, 0, 40), BackgroundColor3 = C.panel, LayoutOrder = order }, parent)
round(row, 9)
stroke(row)
hover(row, C.hover, 0)
local ih = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(11, 12), Size = UDim2.fromOffset(16, 16) }, row)
icon("globe", ih, C.subtext, 15)
text({ Position = UDim2.fromOffset(36, 5), Size = UDim2.new(1, -110, 0, 17),
Text = src.title, TextSize = 12, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, row)
local host = src.url:match("^https?://([^/]+)") or src.url
text({ Position = UDim2.fromOffset(36, 22), Size = UDim2.new(1, -110, 0, 15),
Text = host, TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, row)
local hint = text({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(52, 16), Text = "Copy", TextSize = 11, TextColor3 = C.subtext,
TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false }, row)
row.MouseButton1Click:Connect(function()
if copyToClipboard(src.url) then
hint.Text = "Copied"
task.delay(1.2, function() if row.Parent then hint.Text = "Copy" end end)
end
end)
end
function drawAssistant(msg, index, isLast)
local r = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = index }, column)
local logoHolder = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(26, 26) }, r)
local answering = modelById(msg.answeredById) or msg.model
local logo = brandLogo(logoHolder, answering, 22)
logo.Position = UDim2.fromOffset(2, 2)
local who = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(38, 1),
Size = UDim2.new(1, -38, 0, 22) }, r)
new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6),
VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, who)
text({ Size = UDim2.fromOffset(0, 20), AutomaticSize = Enum.AutomaticSize.X, Text = "Assistant",
Font = Enum.Font.GothamBold, TextSize = 13, TextWrapped = false, LayoutOrder = 1 }, who)
local whoLabel = answering.short or answering.name
if msg.answeredById and msg.fellBackReason and answering ~= msg.model then
whoLabel = whoLabel .. " · for " .. (msg.model.short or msg.model.name)
end
text({ Size = UDim2.fromOffset(0, 20), AutomaticSize = Enum.AutomaticSize.X, Text = whoLabel,
TextSize = 12, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 2 }, who)
local content = new("Frame", { Position = UDim2.fromOffset(38, 26), Size = UDim2.new(1, -38, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1 }, r)
new("UIListLayout", { Padding = UDim.new(0, 10), SortOrder = Enum.SortOrder.LayoutOrder }, content)
local think = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 1, Visible = false }, content)
new("UIListLayout", { Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, think)
local thinkHead = button({ Size = UDim2.fromOffset(0, 24), AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 1 }, think)
new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6),
VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, thinkHead)
local thinkTitle = text({ Size = UDim2.fromOffset(0, 20), AutomaticSize = Enum.AutomaticSize.X,
Text = "Thinking...", TextSize = 13, TextColor3 = C.subtext, Font = Enum.Font.GothamMedium,
TextWrapped = false, LayoutOrder = 1 }, thinkHead)
local chevHolder = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(14, 14), LayoutOrder = 2 }, thinkHead)
local chevIcon = icon("chevron", chevHolder, C.subtext, 14)
local thinkBody = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 2 }, think)
new("Frame", { Size = UDim2.new(0, 2, 1, 0), BackgroundColor3 = C.stroke, BorderSizePixel = 0 }, thinkBody)
local thinkText = text({ Position = UDim2.fromOffset(14, 0), Size = UDim2.new(1, -14, 0, 0),
AutomaticSize = Enum.AutomaticSize.Y, Text = "", TextSize = 13, TextColor3 = C.subtext,
LineHeight = 1.25, RichText = true }, thinkBody)
thinkHead.MouseButton1Click:Connect(function()
msg.thinkOpen = not msg.thinkOpen
if msg._update then msg._update() end
end)
local taskF = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 2, Visible = false }, content)
new("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }, taskF)
local toolsF = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 3, Visible = false }, content)
new("UIListLayout", { Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, toolsF)
local toolSig = ""
local dots = text({ Size = UDim2.new(1, 0, 0, 20), Text = "Thinking", TextSize = 14,
TextColor3 = C.subtext, LayoutOrder = 4, Visible = false }, content)
local bodyC = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 5 }, content)
new("UIListLayout", { Padding = UDim.new(0, 12), SortOrder = Enum.SortOrder.LayoutOrder }, bodyC)
local bodyStroke = stroke(bodyC, C.error)
bodyStroke.Transparency = 1
local bodyPad = pad(bodyC, 0, 0, 0, 0)
local sourcesF = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 6, Visible = false }, content)
new("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }, sourcesF)
local actions = new("Frame", { Size = UDim2.new(1, 0, 0, 24), BackgroundTransparency = 1,
LayoutOrder = 7, Visible = false }, content)
new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 12),
VerticalAlignment = Enum.VerticalAlignment.Center, SortOrder = Enum.SortOrder.LayoutOrder }, actions)
local retry = button({ Size = UDim2.fromOffset(0, 24), AutomaticSize = Enum.AutomaticSize.X,
Text = "Retry", TextSize = 12, TextColor3 = C.subtext, LayoutOrder = 1 }, actions)
retry.MouseEnter:Connect(function() retry.TextColor3 = C.text end)
retry.MouseLeave:Connect(function() retry.TextColor3 = C.subtext end)
retry.MouseButton1Click:Connect(function()
if busy then return end
table.remove(current.messages)
generate()
end)
text({ Size = UDim2.fromOffset(0, 24), AutomaticSize = Enum.AutomaticSize.X, Text = answering.name,
TextSize = 12, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 2 }, actions)
local lastText = nil
local lastSources = nil
local function updateAssistantMessage()
if not r.Parent then return end
local waiting = (msg.status == "thinking" or msg.status == "reasoning" or msg.status == "planning" or msg.status == "editing" or msg.status == "testing")
local elapsed = math.floor(os.clock() - (msg.startClock or os.clock()))
local timeStr = formatTime(elapsed)
local doneTimeStr = formatTime(msg.thinkSecs or 0)
think.Visible = msg.thinkOn == true or (msg.thinking and msg.thinking ~= "")
thinkTitle.Text = waiting and ((msg.taskPhase or "Thinking") .. "... (" .. timeStr .. ")") or ("Thought for " .. doneTimeStr)
thinkBody.Visible = msg.thinkOpen == true
thinkText.Text = richify(msg.thinking or "")
chevIcon.Rotation = msg.thinkOpen and 0 or -90
if msg.taskPhase and msg.status ~= "done" then
taskF.Visible = true
for _, ch in ipairs(taskF:GetChildren()) do
if ch:IsA("GuiObject") then ch:Destroy() end
end
local phaseChip = new("Frame", { Size = UDim2.fromOffset(0, 26), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.panel, LayoutOrder = 1 }, taskF)
round(phaseChip, 13)
stroke(phaseChip)
pad(phaseChip, 0, 12, 0, 10)
text({ Size = UDim2.new(0, 0, 1, 0), AutomaticSize = Enum.AutomaticSize.X,
Text = "Task: " .. tostring(msg.taskPhase), TextSize = 12, TextColor3 = MODELS[modelIndex].accent, TextWrapped = false }, phaseChip)
else
taskF.Visible = false
end
local sig = ""
for _, tl in ipairs(msg.tools or {}) do sig ..= tl.state .. tl.label .. "|" end
if sig ~= toolSig then
toolSig = sig
for _, ch in ipairs(toolsF:GetChildren()) do
if ch:IsA("GuiObject") then ch:Destroy() end
end
for i, tl in ipairs(msg.tools or {}) do drawToolChip(toolsF, tl, i) end
end
toolsF.Visible = msg.tools ~= nil and #msg.tools > 0
dots.Visible = (msg.status == "thinking" or msg.status == "planning") and not msg.thinkOn
dots.Text = (msg.taskPhase or "Thinking") .. string.rep(".", msg.dots or 0)
if msg.text ~= lastText then
lastText = msg.text
renderContent(bodyC, msg.text)
if msg.isError then
for _, d in ipairs(bodyC:GetDescendants()) do
if d:IsA("TextLabel") then d.TextColor3 = C.error end
end
end
end
local srcKey = (msg.status == "done" and not msg.isError) and msg.text or nil
if srcKey ~= lastSources then
lastSources = srcKey
for _, ch in ipairs(sourcesF:GetChildren()) do
if ch:IsA("GuiObject") then ch:Destroy() end
end
local srcs = srcKey and extractSources(srcKey) or {}
if #srcs > 0 then
text({ Size = UDim2.new(1, 0, 0, 16), Text = "Sources", Font = Enum.Font.GothamBold,
TextSize = 12, TextColor3 = C.subtext, LayoutOrder = 1 }, sourcesF)
for i, s in ipairs(srcs) do drawSourceRow(sourcesF, s, i + 1) end
end
sourcesF.Visible = #srcs > 0
end
if msg.isError then
bodyC.BackgroundColor3 = C.error
bodyC.BackgroundTransparency = 0.88
bodyStroke.Transparency = 0.55
bodyPad.PaddingTop = UDim.new(0, 10)
bodyPad.PaddingBottom = UDim.new(0, 10)
bodyPad.PaddingLeft = UDim.new(0, 12)
bodyPad.PaddingRight = UDim.new(0, 12)
else
bodyC.BackgroundTransparency = 1
bodyStroke.Transparency = 1
bodyPad.PaddingTop = UDim.new(0, 0)
bodyPad.PaddingBottom = UDim.new(0, 0)
bodyPad.PaddingLeft = UDim.new(0, 0)
bodyPad.PaddingRight = UDim.new(0, 0)
end
bodyC.Visible = msg.text ~= ""
actions.Visible = msg.status == "done" and isLast
end
msg._update = updateAssistantMessage
updateAssistantMessage()
end
function drawWelcome()
local h = math.max(300, messageArea.AbsoluteSize.Y - 40)
local wrap = new("Frame", { Size = UDim2.new(1, 0, 0, h), BackgroundTransparency = 1 }, column)
local mid = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1 }, wrap)
new("UIListLayout", { Padding = UDim.new(0, 16), SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center }, mid)
local m = MODELS[modelIndex]
local logoModel = settings.autoLogo == false and MODELS[1] or m
local logoHolder = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(56, 56), LayoutOrder = 1 }, mid)
brandLogo(logoHolder, logoModel, 52).Position = UDim2.fromOffset(2, 2)
text({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
Text = "What will we create, " .. player.DisplayName .. "?", Font = SERIF, TextSize = small and 22 or 30,
TextXAlignment = Enum.TextXAlignment.Center, LayoutOrder = 2 }, mid)
text({ Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
Text = "Your ideas. Your game. An agent to help make it happen.", TextSize = 13, TextColor3 = C.subtext,
TextXAlignment = Enum.TextXAlignment.Center, LayoutOrder = 3 }, mid)
local grid = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = 4 }, mid)
new("UIGridLayout", { CellSize = UDim2.new(0.5, -6, 0, 92), CellPadding = UDim2.fromOffset(12, 12),
SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Center }, grid)
local cards = {
{ icon = "search", title = "Explore this game", desc = "Find your bearings in the world.",
prompt = "Give me a quick overview of this game - what is here and what can I do?" },
{ icon = "code", title = "Create something", desc = "Turn an idea into a working script.",
prompt = "Help me turn an idea into a working Luau script." },
{ icon = "settings", title = "Check performance", desc = "Understand FPS, memory, and latency.",
prompt = "Help me check FPS, memory, and latency in this game." },
{ icon = "person", title = "Inspect my character", desc = "See your character's current state.",
prompt = "Inspect my character's current state and stats." },
}
for i, c in ipairs(cards) do
local card = button({ BackgroundColor3 = C.panel, LayoutOrder = i, Text = "", TextXAlignment = Enum.TextXAlignment.Left }, grid)
round(card, 12)
stroke(card)
hover(card, C.hover, 0)
local ih = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(16, 14), Size = UDim2.fromOffset(24, 24) }, card)
icon(c.icon, ih, m.accent, 22)
text({ Position = UDim2.fromOffset(16, 46), Size = UDim2.new(1, -32, 0, 20), Text = c.title,
Font = Enum.Font.GothamMedium, TextSize = 15, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, card)
text({ Position = UDim2.fromOffset(16, 66), Size = UDim2.new(1, -32, 0, 18), Text = c.desc,
TextSize = 12, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, card)
card.MouseButton1Click:Connect(function() send(c.prompt) end)
end
end
local function renderMessages()
for _, ch in ipairs(column:GetChildren()) do
if ch:IsA("GuiObject") then ch:Destroy() end
end
for _, m in ipairs(current.messages) do m._update = nil end
titleLabel.Text = current.title
updateModelChip()
updateHeaderSubtitle()
if #current.messages == 0 then
drawWelcome()
else
for i, m in ipairs(current.messages) do
if m.role == "user" then drawUser(m, i) else drawAssistant(m, i, i == #current.messages) end
end
end
scrollToBottom()
end
local function setSidebar(open, instant)
sidebarOpen = open
sideInner.Size = UDim2.new(0, SIDEBAR_W, 1, 0)
local dur = instant and 0 or 0.22
local function apply(inst, props)
if instant or dur == 0 then
for k, v in pairs(props) do inst[k] = v end
else
tween(inst, dur, props)
end
end
if small then
apply(sidebar, { Position = UDim2.fromOffset(open and 0 or -SIDEBAR_W - 8, 0), Size = UDim2.new(0, SIDEBAR_W, 1, 0) })
apply(main, { Position = UDim2.fromOffset(0, 0), Size = UDim2.new(1, 0, 1, 0) })
if not open then
tween(sidebar, dur, { BackgroundTransparency = 0.15 })
else
sidebar.BackgroundTransparency = 0
end
else
apply(sidebar, { Size = UDim2.new(0, open and SIDEBAR_W or 0, 1, 0) })
apply(main, { Position = UDim2.fromOffset(open and SIDEBAR_W or 0, 0),
Size = UDim2.new(1, open and -SIDEBAR_W or 0, 1, 0) })
end
end
sideCloseBtn.MouseButton1Click:Connect(function()
setSidebar(false)
end)
local function selectConversation(convo)
current = convo
renderMessages()
refreshSidebar()
if small then setSidebar(false) end
end
local function selectProject(proj)
currentProject = proj
conversations = proj.conversations
proj.expanded = true
updateHeaderSubtitle()
refreshSidebar()
end
local function newConversation(forceNew)
local list = currentProject and currentProject.conversations or conversations
if not forceNew and current and #current.messages == 0 then
selectConversation(current)
return
end
local convo = { title = "New chat", messages = {} }
table.insert(list, 1, convo)
selectConversation(convo)
end
local function newProject(name, context)
local p = { name = name or ("Project " .. tostring(#projects + 1)), conversations = {}, context = context or "", expanded = true }
table.insert(projects, 1, p)
return p
end
function newFolder()
local p = newProject("Folder " .. tostring(#projects + 1))
selectProject(p)
newConversation(true)
refreshSidebar()
end
-- Context Menu Logic --
local contextMenu
local function hideContextMenu()
if contextMenu then
contextMenu:Destroy()
contextMenu = nil
end
end
function showContextMenu(x, y, items)
hideContextMenu()
local h = #items * 32 + 8
contextMenu = new("Frame", {
Position = UDim2.fromOffset(x, y),
Size = UDim2.fromOffset(150, h),
BackgroundColor3 = C.panel,
ZIndex = 100
}, gui)
round(contextMenu, 8)
stroke(contextMenu)
new("UIListLayout", { Padding = UDim.new(0, 2), SortOrder = Enum.SortOrder.LayoutOrder }, contextMenu)
new("UIPadding", { PaddingTop = UDim.new(0, 4), PaddingBottom = UDim.new(0, 4) }, contextMenu)
for i, item in ipairs(items) do
local btn = button({
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = 1,
TextXAlignment = Enum.TextXAlignment.Left,
Text = item.text,
TextColor3 = item.color or C.text,
TextSize = 13,
ZIndex = 101,
LayoutOrder = i
}, contextMenu)
pad(btn, 0, 0, 0, 10)
hover(btn, C.hover, 0)
btn.MouseButton1Click:Connect(function()
hideContextMenu()
if item.callback then item.callback() end
end)
end
local catcher = new("TextButton", {
Size = UDim2.fromScale(1,1),
BackgroundTransparency = 1,
ZIndex = 99,
Text = ""
}, gui)
catcher.MouseButton1Click:Connect(hideContextMenu)
task.delay(0.1, function()
if catcher then catcher:Destroy() end
end)
end
local renameUI = {}
function showRenamePrompt(initialText, callback)
if renameUI.overlay then renameUI.overlay:Destroy() end
renameUI.overlay = new("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.5,
ZIndex = 150
}, gui)
local box = new("Frame", {
AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(300, 120),
BackgroundColor3 = C.panel,
ZIndex = 151
}, renameUI.overlay)
round(box, 12)
stroke(box)
text({ Position = UDim2.fromOffset(16, 12), Size = UDim2.new(1, -32, 0, 20), Text = "Rename", Font = Enum.Font.GothamBold, TextSize = 16, ZIndex = 152 }, box)
renameUI.box = new("TextBox", {
Position = UDim2.fromOffset(16, 40),
Size = UDim2.new(1, -32, 0, 32),
BackgroundColor3 = C.bg,
Text = initialText,
TextColor3 = C.text,
TextSize = 13,
Font = Enum.Font.Gotham,
ClearTextOnFocus = false,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 152
}, box)
round(renameUI.box, 6); stroke(renameUI.box, C.stroke); pad(renameUI.box, 0, 10, 0, 10)
local okBtn = button({ Position = UDim2.fromOffset(16, 80), Size = UDim2.new(0.5, -20, 0, 28), BackgroundColor3 = MODELS[modelIndex].accent, Text = "Save", TextSize = 13, ZIndex = 152 }, box)
round(okBtn, 6)
local cancelBtn = button({ Position = UDim2.new(0.5, 4, 0, 80), Size = UDim2.new(0.5, -20, 0, 28), BackgroundColor3 = C.hover, Text = "Cancel", TextSize = 13, ZIndex = 152 }, box)
round(cancelBtn, 6)
okBtn.MouseButton1Click:Connect(function()
callback(renameUI.box.Text)
renameUI.overlay:Destroy()
renameUI.overlay = nil
end)
cancelBtn.MouseButton1Click:Connect(function()
renameUI.overlay:Destroy()
renameUI.overlay = nil
end)
end
function refreshSidebar()
for _, ch in ipairs(convoList:GetChildren()) do
if ch:IsA("GuiObject") then ch:Destroy() end
end
local q = searchBox.Text:lower()
local order = 0
function sideHeader(label, onClick)
order = order + 1
local row = button({ Size = UDim2.new(1, 0, 0, 26), LayoutOrder = order }, convoList)
round(row, 7)
hover(row, C.hover, 0.6)
text({ Position = UDim2.fromOffset(10, 0), Size = UDim2.new(1, -44, 1, 0), Text = label, TextSize = 12,
Font = Enum.Font.GothamBold, TextColor3 = C.subtext, TextWrapped = false }, row)
local add = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -4, 0.5, 0),
Size = UDim2.fromOffset(22, 22) }, row)
round(add, 6)
icon("plus", add, C.subtext, 13)
hover(add, C.hover, 0.4)
add.MouseButton1Click:Connect(onClick)
end
sideHeader("Folders", function() if newFolder then newFolder() end end)
sideHeader("Chats", function() newConversation(true) end)
for _, proj in ipairs(projects) do
local hasMatching = q == "" or proj.name:lower():find(q, 1, true)
if not hasMatching then
for _, c in ipairs(proj.conversations) do
if c.title:lower():find(q, 1, true) then hasMatching = true break end
end
end
if hasMatching then
order = order + 1
-- Folder Header
local header = button({ Size = UDim2.new(1, 0, 0, 32), BackgroundTransparency = 1, LayoutOrder = order, Active = true }, convoList)
round(header, 8)
hover(header, C.hover, 0.5)
local chev = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(2, 8), Size = UDim2.fromOffset(16, 16) }, header)
local chevIcon = icon("chevron", chev, C.subtext, 14)
chevIcon.Rotation = proj.expanded and 0 or -90
local folderIcon = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(22, 8), Size = UDim2.fromOffset(16, 16) }, header)
icon("folder", folderIcon, C.subtext, 16)
text({ Position = UDim2.fromOffset(42, 0), Size = UDim2.new(1, -96, 1, 0), Text = proj.name, Font = Enum.Font.GothamMedium, TextSize = 13, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, header)
local addBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -6, 0.5, 0), Size = UDim2.fromOffset(22, 22), BackgroundTransparency = 1, Active = true }, header)
icon("plus", addBtn, C.subtext, 14)
if #proj.conversations > 0 then
local pill = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -32, 0.5, 0),
Size = UDim2.fromOffset(20, 18), BackgroundColor3 = C.hover, BorderSizePixel = 0, ZIndex = 2 }, header)
round(pill, 9)
text({ Size = UDim2.fromScale(1, 1), Text = tostring(#proj.conversations), TextSize = 11,
TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center, TextWrapped = false, ZIndex = 3 }, pill)
end
header.InputBegan:Connect(function(io)
if io.UserInputType == Enum.UserInputType.MouseButton2 then
showContextMenu(io.Position.X, io.Position.Y, {
{ text = "Rename Folder", callback = function() showRenamePrompt(proj.name, function(newName) proj.name = newName refreshSidebar() end) end },
{ text = "Delete Folder", color = C.error, callback = function()
local idx = table.find(projects, proj)
if idx then table.remove(projects, idx) end
if #projects == 0 then newProject("General Chats") end
if proj == currentProject then selectProject(projects[1]) end
refreshSidebar()
end }
})
end
end)
header.MouseButton1Click:Connect(function()
proj.expanded = not proj.expanded
refreshSidebar()
end)
addBtn.MouseButton1Click:Connect(function()
selectProject(proj)
newConversation(true)
end)
if proj.expanded then
for _, convo in ipairs(proj.conversations) do
if q == "" or convo.title:lower():find(q, 1, true) then
order = order + 1
local selected = convo == current
local item = button({ Size = UDim2.new(1, -16, 0, 30), Position = UDim2.fromOffset(16, 0), BackgroundColor3 = C.hover, BackgroundTransparency = selected and 0 or 1, LayoutOrder = order, Active = true }, convoList)
round(item, 8)
text({ Position = UDim2.fromOffset(12, 0), Size = UDim2.new(1, -36, 1, 0), Text = convo.title, TextSize = 13, TextColor3 = selected and C.text or C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd }, item)
local del = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -6, 0.5, 0), Size = UDim2.fromOffset(24, 24), Visible = selected, BackgroundTransparency = 1, Active = true }, item)
local dots = new("Frame", { BackgroundTransparency = 1, Size = UDim2.fromOffset(16, 16), Position = UDim2.fromOffset(4, 4) }, del)
for i = 1, 3 do
local dot = new("Frame", { BackgroundColor3 = C.subtext, Size = UDim2.fromOffset(2, 2), Position = UDim2.fromOffset(0, (i-2)*5), BorderSizePixel = 0 }, dots)
round(dot, 1)
end
item.InputBegan:Connect(function(io)
if io.UserInputType == Enum.UserInputType.MouseButton2 then
showContextMenu(io.Position.X, io.Position.Y, {
{ text = "Rename Chat", callback = function() showRenamePrompt(convo.title, function(newName) convo.title = newName refreshSidebar() if convo == current then titleLabel.Text = convo.title end end) end },
{ text = "Copy Text", callback = function()
local allText = ""
for _, m in ipairs(convo.messages) do
if m.text then allText = allText .. m.text .. "\n\n" end
end
copyToClipboard(allText)
end },
{ text = "Delete Chat", color = C.error, callback = function()
if convo == current and busy then cancelFlag = true end
local idx = table.find(proj.conversations, convo)
if idx then table.remove(proj.conversations, idx) end
if convo == current then
if #proj.conversations == 0 then
local c = { title = "New chat", messages = {} }
table.insert(proj.conversations, c)
selectConversation(c)
else
selectConversation(proj.conversations[math.min(idx, #proj.conversations)])
end
else
refreshSidebar()
end
end }
})
end
end)
item.MouseEnter:Connect(function()
del.Visible = true
if not selected then tween(item, 0.1, { BackgroundTransparency = 0.5 }) end
end)
item.MouseLeave:Connect(function()
del.Visible = selected
if not selected then tween(item, 0.1, { BackgroundTransparency = 1 }) end
end)
item.MouseButton1Click:Connect(function()
selectProject(proj)
selectConversation(convo)
end)
del.MouseButton1Click:Connect(function()
if convo == current and busy then cancelFlag = true end
local idx = table.find(proj.conversations, convo)
if idx then table.remove(proj.conversations, idx) end
if convo == current then
if #proj.conversations == 0 then
local c = { title = "New chat", messages = {} }
table.insert(proj.conversations, c)
selectConversation(c)
else
selectConversation(proj.conversations[math.min(idx, #proj.conversations)])
end
else
refreshSidebar()
end
end)
end
end
end
end
end
end
local function updateThinkPill()
local m = MODELS[modelIndex]
thinkLabel.Text = m.think
if thinkOn then
thinkPill.BackgroundColor3 = m.accent
thinkPill.BackgroundTransparency = 0.82
thinkStroke.Color = m.accent
thinkLabel.TextColor3 = m.accent
thinkDot.BackgroundColor3 = m.accent
else
thinkPill.BackgroundTransparency = 1
thinkStroke.Color = C.stroke
thinkLabel.TextColor3 = C.subtext
thinkDot.BackgroundColor3 = C.subtext
end
end
local function applyTheme(name)
local palette = THEMES[name] or THEMES["Claude"]
settings.theme = THEMES[name] and name or "Claude"
local oldPalette = ACTIVE_PALETTE
for key, value in pairs(palette) do C[key] = value end
ACTIVE_PALETTE = palette
local function replaceColor(color)
for key, oldColor in pairs(oldPalette) do
if color == oldColor then
return palette[key] or color
end
end
return color
end
for _, obj in ipairs(gui:GetDescendants()) do
if obj:IsA("GuiObject") then
obj.BackgroundColor3 = replaceColor(obj.BackgroundColor3)
if obj:IsA("TextLabel") or obj:IsA("TextButton") or obj:IsA("TextBox") then
obj.TextColor3 = replaceColor(obj.TextColor3)
if obj:IsA("TextBox") then
obj.PlaceholderColor3 = replaceColor(obj.PlaceholderColor3)
end
end
if obj:IsA("ImageLabel") or obj:IsA("ImageButton") then
obj.ImageColor3 = replaceColor(obj.ImageColor3)
end
end
if obj:IsA("UIStroke") then
obj.Color = replaceColor(obj.Color)
end
end
window.BackgroundColor3 = C.bg
sidebar.BackgroundColor3 = C.sidebar
composer.BackgroundColor3 = C.panel
searchBox.BackgroundColor3 = C.bg
messageArea.ScrollBarImageColor3 = C.stroke
convoList.ScrollBarImageColor3 = C.stroke
userCard.BackgroundColor3 = C.panel
avatar.BackgroundColor3 = C.hover
input.TextColor3 = C.text
input.PlaceholderColor3 = C.subtext
searchBox.TextColor3 = C.text
searchBox.PlaceholderColor3 = C.subtext
updateModelChip()
updateThinkPill()
updateHeaderSubtitle()
pcall(function() if current then renderMessages() end end)
pcall(function() refreshSidebar() end)
pcall(function() layoutSidebar() end)
end
local function applyModel()
local m = MODELS[modelIndex]
if settings.customModel and settings.customModel.id ~= m.id then
settings.customModel = nil
settings.modelId = nil
saveSettings()
end
if API.blockedModels then API.blockedModels = {} end
updateModelChip()
local logoModel = settings.autoLogo == false and MODELS[1] or m
if brandSpark then brandSpark:Destroy() end
brandSpark = brandLogo(sideInner, logoModel, 22)
brandSpark.Position = UDim2.fromOffset(18, 17)
if toggleSpark then toggleSpark:Destroy() end
toggleSpark = brandLogo(toggleBtn, logoModel, 22)
toggleSpark.Position = UDim2.fromOffset(12, 12)
input.PlaceholderText = "Message " .. m.brandName .. "..."
if sideProviderLabel then sideProviderLabel.Text = providerLabel(m) end
sendBtn.BackgroundColor3 = m.accent
recolor(plusIcon, m.accent)
updateThinkPill()
updateHeaderSubtitle()
if workspaceRefresh then workspaceRefresh() end
if current and #current.messages == 0 then renderMessages() end
end
thinkPill.MouseButton1Click:Connect(function()
thinkOn = not thinkOn
updateThinkPill()
end)
local menu, catcher
local function closeMenu()
if menu then
local m = menu
menu = nil
tween(m, 0.12, { BackgroundTransparency = 1 })
for _, d in ipairs(m:GetDescendants()) do
if d:IsA("GuiObject") then
pcall(function()
tween(d, 0.1, { BackgroundTransparency = 1, TextTransparency = 1 })
end)
end
end
task.delay(0.14, function() if m.Parent then m:Destroy() end end)
end
if catcher then catcher:Destroy() catcher = nil end
end
function openMenu()
if menu then closeMenu() return end
catcher = button({ Size = UDim2.fromScale(1, 1), ZIndex = 50 }, window)
catcher.MouseButton1Click:Connect(closeMenu)
local w, itemH = 220, 40
local maxH = math.min((#MODELS + #API.CATALOGUE_ORDER) * itemH + 10, 360)
menu = new("Frame", { Size = UDim2.fromOffset(w, maxH), BackgroundColor3 = C.panel, ZIndex = 51 }, window)
round(menu, 12)
stroke(menu)
menu.ClipsDescendants = true
local scroll = new("ScrollingFrame", {
Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0,
ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 52,
}, menu)
local x = modelBtn.AbsolutePosition.X + modelBtn.AbsoluteSize.X - window.AbsolutePosition.X - w
local y = modelBtn.AbsolutePosition.Y - window.AbsolutePosition.Y - maxH - 8
menu.Position = UDim2.fromOffset(math.max(8, x), math.max(8, y))
menu.BackgroundTransparency = 1
tween(menu, 0.14, { BackgroundTransparency = 0 })
for i, m in ipairs(MODELS) do
local it = button({ Position = UDim2.fromOffset(4, 4 + (i - 1) * itemH), Size = UDim2.new(1, -8, 0, itemH - 4),
BackgroundColor3 = C.hover, BackgroundTransparency = i == modelIndex and 0 or 1, ZIndex = 53 }, scroll)
round(it, 8)
hover(it, C.hover, 0)
local lh = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(8, 8),
Size = UDim2.fromOffset(18, 18), ZIndex = 54 }, it)
brandLogo(lh, m, 18)
text({ Position = UDim2.fromOffset(32, 2), Size = UDim2.new(1, -56, 0, 16), Text = m.short,
Font = Enum.Font.GothamMedium, TextSize = 12, TextWrapped = false, ZIndex = 54 }, it)
text({ Position = UDim2.fromOffset(32, 18), Size = UDim2.new(1, -56, 0, 14), Text = m.desc,
TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, ZIndex = 54 }, it)
if i == modelIndex then
local ck = new("Frame", { BackgroundTransparency = 1, AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -8, 0.5, 0), Size = UDim2.fromOffset(14, 14), ZIndex = 54 }, it)
icon("check", ck, m.accent, 14)
end
it.MouseButton1Click:Connect(function()
modelIndex = i
closeMenu()
applyModel()
end)
end
for i, key in ipairs(API.CATALOGUE_ORDER) do
local spec = API.CATALOGUES[key]
local catRow = button({ Position = UDim2.fromOffset(4, 4 + (#MODELS + i - 1) * itemH),
Size = UDim2.new(1, -8, 0, itemH - 4), BackgroundColor3 = C.hover,
BackgroundTransparency = 1, ZIndex = 53 }, scroll)
round(catRow, 8)
hover(catRow, C.hover, 0)
local catIcon = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(8, 8),
Size = UDim2.fromOffset(18, 18), ZIndex = 54 }, catRow)
icon("search", catIcon, C.subtext, 16)
text({ Position = UDim2.fromOffset(32, 2), Size = UDim2.new(1, -56, 0, 16), Text = spec.title,
Font = Enum.Font.GothamMedium, TextSize = 12, TextWrapped = false, ZIndex = 54 }, catRow)
text({ Position = UDim2.fromOffset(32, 18), Size = UDim2.new(1, -56, 0, 14), Text = spec.blurb,
TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, ZIndex = 54 }, catRow)
catRow.MouseButton1Click:Connect(function()
closeMenu()
openCataloguePicker(key)
end)
end
end
modelBtn.MouseButton1Click:Connect(openMenu)
function placeholderFor(m) return "Message " .. m.brandName .. "..." end
local function flashPlaceholder(msgText)
input.PlaceholderText = msgText
task.delay(3, function() input.PlaceholderText = placeholderFor(MODELS[modelIndex]) end)
end
function refreshAttachStrip()
for _, ch in ipairs(attachRow:GetChildren()) do
if ch:IsA("GuiObject") then ch:Destroy() end
end
for i, att in ipairs(attachments) do
local th = new("ImageLabel", { Size = UDim2.fromOffset(100, 58), BackgroundColor3 = C.bg,
ScaleType = Enum.ScaleType.Crop, LayoutOrder = i }, attachRow)
round(th, 10)
stroke(th)
pcall(function() th.ImageContent = Content.fromObject(att.editable) end)
local x = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -3, 0, 3),
Size = UDim2.fromOffset(18, 18), BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 0.35 }, th)
round(x, 9)
icon("cross", x, Color3.new(1, 1, 1), 10)
x.MouseButton1Click:Connect(function()
table.remove(attachments, i)
refreshAttachStrip()
end)
end
updateComposer()
end
function attachScreenshot()
if #attachments >= MAX_ATTACH then
flashPlaceholder("Max " .. MAX_ATTACH .. " images per message")
return
end
flashPlaceholder("Capturing screenshot...")
local att, err = captureAttachment()
if not att then
flashPlaceholder("Screenshot failed: " .. tostring(err):sub(1, 70))
return
end
table.insert(attachments, att)
input.PlaceholderText = placeholderFor(MODELS[modelIndex])
refreshAttachStrip()
end
local function makeSwitch(parent, on, accent)
local function pos(v) return UDim2.new(0, v and 17 or 3, 0.5, 0) end
local sw = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -12, 0.5, 0),
Size = UDim2.fromOffset(34, 20), BackgroundColor3 = on and accent or C.stroke }, parent)
round(sw, 10)
local knob = new("Frame", { AnchorPoint = Vector2.new(0, 0.5), Position = pos(on),
Size = UDim2.fromOffset(14, 14), BackgroundColor3 = Color3.new(1, 1, 1) }, sw)
round(knob, 7)
return function(v)
tween(sw, 0.12, { BackgroundColor3 = v and accent or C.stroke })
tween(knob, 0.12, { Position = pos(v) })
end
end
function openToolsMenu()
if menu then closeMenu() return end
catcher = button({ Size = UDim2.fromScale(1, 1), ZIndex = 50 }, window)
catcher.MouseButton1Click:Connect(closeMenu)
local w, itemH = 300, 56
local rows = {
{ kind = "action", icon = "image", title = "Screenshot the game", desc = "Attach what you see" },
{ kind = "toggle", key = "console", icon = "terminal", title = "Console access", desc = "AI may read errors when needed" },
{ kind = "toggle", key = "screen", icon = "image", title = "Game vision", desc = "AI may look at the game itself" },
{ kind = "toggle", key = "web", icon = "globe", title = "Web search", desc = "AI may look things up online" },
}
local h = #rows * itemH + 12
menu = new("Frame", { Size = UDim2.fromOffset(w, h), BackgroundColor3 = C.panel, ZIndex = 51 }, window)
round(menu, 14)
stroke(menu)
local x = plusBtn.AbsolutePosition.X - window.AbsolutePosition.X
local y = plusBtn.AbsolutePosition.Y - window.AbsolutePosition.Y - h - 8
menu.Position = UDim2.fromOffset(math.max(8, math.min(x, window.AbsoluteSize.X - w - 8)), math.max(8, y))
local accent = MODELS[modelIndex].accent
for i, rw in ipairs(rows) do
local it = button({ Position = UDim2.fromOffset(6, 6 + (i - 1) * itemH), Size = UDim2.new(1, -12, 0, itemH - 4),
BackgroundColor3 = C.hover, BackgroundTransparency = 1 }, menu)
round(it, 10)
hover(it, C.hover, 0)
local ih = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(12, 14),
Size = UDim2.fromOffset(22, 22) }, it)
icon(rw.icon, ih, C.text, 20)
text({ Position = UDim2.fromOffset(46, 7), Size = UDim2.new(1, -110, 0, 20), Text = rw.title,
Font = Enum.Font.GothamMedium, TextSize = 14, TextWrapped = false }, it)
text({ Position = UDim2.fromOffset(46, 27), Size = UDim2.new(1, -110, 0, 16), Text = rw.desc,
TextSize = 12, TextColor3 = C.subtext, TextWrapped = false }, it)
if rw.kind == "toggle" then
local setSwitch = makeSwitch(it, settings[rw.key], accent)
it.MouseButton1Click:Connect(function()
settings[rw.key] = not settings[rw.key]
setSwitch(settings[rw.key])
end)
else
it.MouseButton1Click:Connect(function()
closeMenu()
task.spawn(attachScreenshot)
end)
end
end
end
plusBtn.MouseButton1Click:Connect(openToolsMenu)
function openPermissionMenu()
if menu then closeMenu() return end
catcher = button({ Size = UDim2.fromScale(1, 1), ZIndex = 50 }, window)
catcher.MouseButton1Click:Connect(closeMenu)
local w, itemH = 300, 58
local h = #PERMISSION_MODES * itemH + 12
menu = new("Frame", { Size = UDim2.fromOffset(w, h), BackgroundColor3 = C.panel, ZIndex = 51 }, window)
round(menu, 14)
stroke(menu)
local x = moreBtn.AbsolutePosition.X + moreBtn.AbsoluteSize.X - window.AbsolutePosition.X - w
local y = moreBtn.AbsolutePosition.Y - window.AbsolutePosition.Y - h - 8
menu.Position = UDim2.fromOffset(math.max(8, math.min(x, window.AbsoluteSize.X - w - 8)), math.max(8, y))
local accent = MODELS[modelIndex].accent
for i, mode in ipairs(PERMISSION_MODES) do
local selected = settings.permission == mode.id
local it = button({ Position = UDim2.fromOffset(6, 6 + (i - 1) * itemH), Size = UDim2.new(1, -12, 0, itemH - 4),
BackgroundColor3 = C.hover, BackgroundTransparency = selected and 0 or 1, Text = "",
TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 52 }, menu)
round(it, 10)
hover(it, C.hover, 0)
local titleColor = mode.accent and accent or (selected and C.text or C.subtext)
text({ Position = UDim2.fromOffset(14, 8), Size = UDim2.new(1, -44, 0, 20), Text = mode.title,
Font = Enum.Font.GothamMedium, TextSize = 14, TextColor3 = titleColor, ZIndex = 53 }, it)
text({ Position = UDim2.fromOffset(14, 28), Size = UDim2.new(1, -44, 0, 16), Text = mode.desc,
TextSize = 11, TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 53 }, it)
if selected then
local ck = icon("check", it, accent, 16)
ck.AnchorPoint = Vector2.new(1, 0.5)
ck.Position = UDim2.new(1, -14, 0.5, 0)
ck.ZIndex = 53
end
it.MouseButton1Click:Connect(function()
settings.permission = mode.id
closeMenu()
end)
end
end
moreBtn.MouseButton1Click:Connect(openPermissionMenu)
local settingsPanel
function openSettings(initialTab)
if settingsPanel then
if API.settingsUI and API.settingsUI.close and API.settingsUI.isOpen
and API.settingsUI.isOpen() then
API.settingsUI.close()
return
end
local old = settingsPanel
settingsPanel = nil
tween(old, 0.12, { BackgroundTransparency = 1 })
task.delay(0.14, function() if old.Parent then old:Destroy() end end)
return
end
closeMenu()
local accent = MODELS[modelIndex].accent
local overlay = new("Frame", { Name = "SettingsOverlay", Size = UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.new(0, 0, 0), BackgroundTransparency = 1, ZIndex = 60 }, window)
settingsPanel = overlay
local catcher = button({ Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, ZIndex = 60 }, overlay)
local panel = new("Frame", { Name = "SettingsPanel", AnchorPoint = Vector2.new(0.5, 0.5),
Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.new(0.94, 0, 0.92, 0),
BackgroundColor3 = C.panel, ZIndex = 61 }, overlay)
round(panel, 16)
stroke(panel)
new("UISizeConstraint", { MinSize = Vector2.new(600, 420), MaxSize = Vector2.new(1000, 680) }, panel)
local panelScale = new("UIScale", { Scale = 0.97 }, panel)
tween(overlay, 0.16, { BackgroundTransparency = 0.45 })
tween(panelScale, 0.18, { Scale = 1 })
local closing = false
local keyConn
local function closePanel()
if closing then return end
closing = true
if settingsPanel == overlay then settingsPanel = nil end
if keyConn then pcall(function() keyConn:Disconnect() end) end
tween(panelScale, 0.13, { Scale = 0.96 })
tween(overlay, 0.13, { BackgroundTransparency = 1 })
task.delay(0.15, function() if overlay.Parent then overlay:Destroy() end end)
end
catcher.MouseButton1Click:Connect(closePanel)
text({ Position = UDim2.fromOffset(22, 13), Size = UDim2.new(1, -80, 0, 24), Text = "Settings",
Font = Enum.Font.GothamBold, TextSize = 19, ZIndex = 62 }, panel)
text({ Position = UDim2.fromOffset(22, 37), Size = UDim2.new(1, -80, 0, 16),
Text = "Changes apply immediately · Save keeps them for next time", TextSize = 11,
TextColor3 = C.subtext, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd,
ZIndex = 62 }, panel)
local close = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -12, 0, 12),
Size = UDim2.fromOffset(30, 30), BackgroundColor3 = C.hover, ZIndex = 62 }, panel)
round(close, 8)
hover(close, C.hover, 0)
icon("cross", close, C.subtext, 14)
close.MouseButton1Click:Connect(closePanel)
new("Frame", { Position = UDim2.fromOffset(0, 64), Size = UDim2.new(1, 0, 0, 1),
BackgroundColor3 = C.stroke, BorderSizePixel = 0, ZIndex = 62 }, panel)
local navCol = new("Frame", { Position = UDim2.fromOffset(12, 78), Size = UDim2.new(0, 200, 1, -146),
BackgroundTransparency = 1, ZIndex = 62 }, panel)
local navSearch = new("TextBox", { Size = UDim2.new(1, 0, 0, 32), BackgroundColor3 = C.bg,
Text = "", PlaceholderText = "Search settings", PlaceholderColor3 = C.subtext,
TextColor3 = C.text, TextSize = 12, Font = Enum.Font.Gotham, ClearTextOnFocus = false,
TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 63 }, navCol)
round(navSearch, 8)
stroke(navSearch, C.stroke)
pad(navSearch, 0, 10, 0, 32)
local searchIcon = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(11, 10),
Size = UDim2.fromOffset(14, 14), ZIndex = 64 }, navSearch)
icon("search", searchIcon, C.subtext, 14)
local navScroll = new("ScrollingFrame", { Position = UDim2.fromOffset(0, 42),
Size = UDim2.new(1, 0, 1, -42), BackgroundTransparency = 1, BorderSizePixel = 0,
ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(),
ZIndex = 63 }, navCol)
local content = new("ScrollingFrame", { Position = UDim2.fromOffset(228, 78),
Size = UDim2.new(1, -246, 1, -146), BackgroundTransparency = 1, BorderSizePixel = 0,
ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 62 }, panel)
new("UIListLayout", { Padding = UDim.new(0, 16), SortOrder = Enum.SortOrder.LayoutOrder }, content)
pad(content, 4, 10, 20, 0)
local PAGES = {
{ id = "General", icon = "settings", group = "Assistant",
desc = "Workspace, agent behaviour and startup" },
{ id = "Personality", icon = "person", group = "Assistant",
desc = "Extra instructions added to every reply" },
{ id = "Providers", icon = "globe", group = "Models",
desc = "Where requests go and which model answers" },
{ id = "Appearance", icon = "image", group = "Workspace",
desc = "Theme, motion and interface scale" },
{ id = "Permissions", icon = "terminal", group = "Workspace",
desc = "What the assistant may read, run and change" },
{ id = "API keys", icon = "code", group = "Account",
desc = "Provider credentials and where they live" },
}
local PAGE_INFO = {}
for _, page in ipairs(PAGES) do PAGE_INFO[page.id] = page end
local SEARCH = {
{ page = "General", title = "Auto compact", desc = "Fold older turns into a summary" },
{ page = "General", title = "Logo follows model", desc = "Show the selected model's mark" },
{ page = "General", title = "Detected game", desc = "Context sent with every request" },
{ page = "Personality", title = "Personality", desc = "Custom instructions for the assistant" },
{ page = "Personality", title = "Starting points", desc = "Preset instruction chips" },
{ page = "Providers", title = "Provider", desc = "OpenCode Zen or AgentRouter" },
{ page = "Providers", title = "Active model", desc = "Chosen model for this session" },
{ page = "Providers", title = "OpenCode key", desc = "Free models use the shared pool" },
{ page = "Providers", title = "AgentRouter key", desc = "Required for Claude and GPT models" },
{ page = "Appearance", title = "Theme", desc = "Palette used across chats and panels" },
{ page = "Appearance", title = "Motion", desc = "Speed of panels, tabs and menus" },
{ page = "Appearance", title = "Interface scale", desc = "Zoom the window contents" },
{ page = "Appearance", title = "Transcript width", desc = "How wide assistant replies can grow" },
{ page = "Permissions", title = "Console access", desc = "Read output the game logs" },
{ page = "Permissions", title = "Game vision", desc = "Capture the screen for visual context" },
{ page = "Permissions", title = "Web search", desc = "Let the model look things up online" },
{ page = "Permissions", title = "Read only", desc = "Inspection only, nothing changes" },
{ page = "Permissions", title = "Ask first", desc = "Writes wait for confirmation" },
{ page = "Permissions", title = "Auto", desc = "Writes run, execution still asks" },
{ page = "Permissions", title = "Allow everything", desc = "No prompts at all" },
{ page = "API keys", title = "Settings file", desc = "Where keys and preferences are stored" },
}
local navButtons, builders = {}, {}
local rowOrder, cardCount = 0, {}
local openPage, currentPage = nil, "General"
local suppressSearch = false
local function indexOf(list, value)
for i, item in ipairs(list) do
if item == value then return i end
end
return 1
end
local function clearContent()
for _, child in ipairs(content:GetChildren()) do
if child:IsA("GuiObject") then child:Destroy() end
end
rowOrder, cardCount = 0, {}
end
local function pageHead(title, blurb)
rowOrder = rowOrder + 1
local head = new("Frame", { Size = UDim2.new(1, 0, 0, 44), BackgroundTransparency = 1,
LayoutOrder = rowOrder }, content)
text({ Position = UDim2.fromOffset(0, 2), Size = UDim2.new(1, 0, 0, 20), Text = title,
Font = Enum.Font.GothamBold, TextSize = 17, ZIndex = 64 }, head)
text({ Position = UDim2.fromOffset(0, 24), Size = UDim2.new(1, 0, 0, 16), Text = blurb or "",
TextSize = 11, TextColor3 = C.subtext, TextWrapped = false,
TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, head)
end
local function section(label)
rowOrder = rowOrder + 1
local wrap = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = rowOrder }, content)
new("UIListLayout", { Padding = UDim.new(0, 6), SortOrder = Enum.SortOrder.LayoutOrder }, wrap)
text({ Size = UDim2.new(1, 0, 0, 14), Text = label:upper(), Font = Enum.Font.GothamBold,
TextSize = 10, TextColor3 = C.subtext, TextWrapped = false, LayoutOrder = 1,
ZIndex = 64 }, wrap)
local card = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundColor3 = C.bg, LayoutOrder = 2 }, wrap)
round(card, 10)
stroke(card, C.stroke)
new("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder }, card)
cardCount[card] = 0
return card
end
local function row(card, title, desc, height)
cardCount[card] = (cardCount[card] or 0) + 1
local index = cardCount[card]
local r = new("Frame", { Size = UDim2.new(1, 0, 0, height or 58), BackgroundTransparency = 1,
LayoutOrder = index }, card)
pad(r, 0, 14, 0, 14)
if index > 1 then
new("Frame", { Position = UDim2.new(0, 14, 0, 0), Size = UDim2.new(1, -28, 0, 1),
BackgroundColor3 = C.stroke, BackgroundTransparency = 0.5, BorderSizePixel = 0,
ZIndex = 63 }, r)
end
text({ Position = UDim2.fromOffset(0, 9), Size = UDim2.new(1, -216, 0, 18), Text = title,
TextSize = 13, TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd,
ZIndex = 64 }, r)
if desc and desc ~= "" then
text({ Position = UDim2.fromOffset(0, 27), Size = UDim2.new(1, -216, 0, 16), Text = desc,
TextSize = 10, TextColor3 = C.subtext, TextWrapped = false,
TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, r)
end
return r
end
local function pill(parent, value, color, y)
local box = new("Frame", { AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, y or 10),
Size = UDim2.fromOffset(0, 22), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.panel, ZIndex = 64 }, parent)
round(box, 11)
stroke(box, C.stroke)
pad(box, 0, 10, 0, 10)
text({ Size = UDim2.fromOffset(0, 22), AutomaticSize = Enum.AutomaticSize.X, Text = value,
TextSize = 10, TextColor3 = color or C.subtext, TextWrapped = false, ZIndex = 65 }, box)
return box
end
local function segmented(parent, options, selectedIndex, onPick)
local box = new("Frame", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(4, 30), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.panel, ZIndex = 64 }, parent)
round(box, 9)
stroke(box, C.stroke)
new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 2),
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder }, box)
pad(box, 2, 2, 2, 2)
local chips = {}
local function select(i, silent)
for idx, chip in ipairs(chips) do
tween(chip, 0.14, { BackgroundTransparency = idx == i and 0 or 1,
TextColor3 = idx == i and C.text or C.subtext })
end
if not silent and onPick then onPick(i) end
end
for i, label in ipairs(options) do
local chip = button({ Size = UDim2.fromOffset(0, 26), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.hover, BackgroundTransparency = i == selectedIndex and 0 or 1,
Text = label, TextSize = 12, TextWrapped = false,
TextColor3 = i == selectedIndex and C.text or C.subtext,
LayoutOrder = i, ZIndex = 65 }, box)
round(chip, 7)
pad(chip, 0, 12, 0, 12)
chips[i] = chip
chip.MouseButton1Click:Connect(function() select(i) end)
end
return select
end
local function toggle(parent, value, onChange)
local on = value and true or false
local track = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(40, 22), BackgroundColor3 = on and accent or C.hover,
ZIndex = 64 }, parent)
round(track, 11)
local knob = new("Frame", { AnchorPoint = Vector2.new(0, 0.5),
Position = UDim2.new(0, on and 19 or 3, 0.5, 0), Size = UDim2.fromOffset(16, 16),
BackgroundColor3 = Color3.new(1, 1, 1), BorderSizePixel = 0, ZIndex = 65 }, track)
round(knob, 8)
track.MouseButton1Click:Connect(function()
on = not on
tween(track, 0.14, { BackgroundColor3 = on and accent or C.hover })
tween(knob, 0.16, { Position = UDim2.new(0, on and 19 or 3, 0.5, 0) })
if onChange then onChange(on) end
end)
return track
end
local function optionRow(card, title, desc, selected, onPick)
cardCount[card] = (cardCount[card] or 0) + 1
local index = cardCount[card]
local r = button({ Size = UDim2.new(1, 0, 0, 64), BackgroundTransparency = 1,
LayoutOrder = index, ZIndex = 63 }, card)
pad(r, 0, 14, 0, 14)
if index > 1 then
new("Frame", { Position = UDim2.new(0, 14, 0, 0), Size = UDim2.new(1, -28, 0, 1),
BackgroundColor3 = C.stroke, BackgroundTransparency = 0.5, BorderSizePixel = 0 }, r)
end
text({ Position = UDim2.fromOffset(0, 12), Size = UDim2.new(1, -58, 0, 18), Text = title,
TextSize = 13, ZIndex = 64 }, r)
text({ Position = UDim2.fromOffset(0, 31), Size = UDim2.new(1, -58, 0, 16), Text = desc,
TextSize = 10, TextColor3 = C.subtext, TextWrapped = false,
TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, r)
local dot = new("Frame", { AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, 0, 0.5, 0), Size = UDim2.fromOffset(20, 20),
BackgroundColor3 = selected and accent or C.panel,
BackgroundTransparency = selected and 0 or 1, BorderSizePixel = 0, ZIndex = 64 }, r)
round(dot, 10)
if selected then
icon("check", dot, Color3.new(1, 1, 1), 12)
else
stroke(dot, C.stroke, 1.5)
end
r.MouseButton1Click:Connect(function() onPick() end)
end
local function themeGrid(card)
cardCount[card] = (cardCount[card] or 0) + 1
local holder = new("Frame", { Size = UDim2.new(1, 0, 0, 0), AutomaticSize = Enum.AutomaticSize.Y,
BackgroundTransparency = 1, LayoutOrder = cardCount[card] }, card)
pad(holder, 12, 14, 10, 14)
new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Wraps = true,
Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder }, holder)
local borders = {}
local function paint(name)
for key, border in pairs(borders) do
border.Color = key == name and accent or C.stroke
border.Thickness = key == name and 2 or 1
end
end
for i, name in ipairs({ "Claude", "ChatGPT", "DeepSeek", "GLM", "Kimi" }) do
local pal = THEMES[name]
local b = button({ Size = UDim2.fromOffset(94, 68), BackgroundColor3 = pal.bg,
LayoutOrder = i, ZIndex = 64 }, holder)
round(b, 8)
borders[name] = stroke(b, C.stroke)
local lineA = new("Frame", { Position = UDim2.fromOffset(10, 12), Size = UDim2.fromOffset(42, 7),
BackgroundColor3 = pal.text, BorderSizePixel = 0, ZIndex = 65 }, b)
round(lineA, 3)
local lineB = new("Frame", { Position = UDim2.fromOffset(10, 24), Size = UDim2.fromOffset(60, 5),
BackgroundColor3 = pal.subtext, BorderSizePixel = 0, ZIndex = 65 }, b)
round(lineB, 2)
local swatch = new("Frame", { Position = UDim2.fromOffset(10, 38), Size = UDim2.fromOffset(30, 16),
BackgroundColor3 = pal.panel, BorderSizePixel = 0, ZIndex = 65 }, b)
round(swatch, 4)
text({ Position = UDim2.fromOffset(10, 51), Size = UDim2.new(1, -16, 0, 12), Text = name,
TextSize = 9, TextColor3 = pal.subtext, TextWrapped = false, ZIndex = 65 }, b)
b.MouseButton1Click:Connect(function()
applyTheme(name)
settings.theme = name
paint(name)
end)
end
paint(settings.theme)
end
local function openModelPicker()
local layer = new("Frame", { Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.45, ZIndex = 80 }, panel)
local backdrop = button({ Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1,
ZIndex = 80 }, layer)
local box = new("Frame", { AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5),
Size = UDim2.fromOffset(360, 380), BackgroundColor3 = C.panel, ZIndex = 81 }, layer)
round(box, 14)
stroke(box)
text({ Position = UDim2.fromOffset(16, 12), Size = UDim2.new(1, -32, 0, 20), Text = "Choose a model",
Font = Enum.Font.GothamBold, TextSize = 15, ZIndex = 82 }, box)
local list = new("ScrollingFrame", { Position = UDim2.fromOffset(8, 40),
Size = UDim2.new(1, -16, 1, -48), BackgroundTransparency = 1, BorderSizePixel = 0,
ScrollBarThickness = 3, ScrollBarImageColor3 = C.stroke, CanvasSize = UDim2.new(),
AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 82 }, box)
new("UIListLayout", { Padding = UDim.new(0, 2) }, list)
for i, m in ipairs(MODELS) do
local item = button({ Size = UDim2.new(1, 0, 0, 46), BackgroundColor3 = C.hover,
BackgroundTransparency = i == modelIndex and 0 or 1, LayoutOrder = i,
ZIndex = 82 }, list)
round(item, 8)
hover(item, C.hover, 0.1)
local dot = new("Frame", { Position = UDim2.fromOffset(10, 14), Size = UDim2.fromOffset(18, 18),
BackgroundColor3 = m.accent, BorderSizePixel = 0, ZIndex = 83 }, item)
round(dot, 9)
text({ Position = UDim2.fromOffset(36, 6), Size = UDim2.new(1, -60, 0, 16),
Text = m.brandName .. " " .. m.short, TextSize = 12, ZIndex = 83 }, item)
text({ Position = UDim2.fromOffset(36, 23), Size = UDim2.new(1, -60, 0, 14), Text = m.desc,
TextSize = 10, TextColor3 = C.subtext, ZIndex = 83 }, item)
if i == modelIndex then
local mark = new("Frame", { AnchorPoint = Vector2.new(1, 0.5),
Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(14, 14),
BackgroundTransparency = 1, ZIndex = 83 }, item)
icon("check", mark, m.accent, 14)
end
item.MouseButton1Click:Connect(function()
modelIndex = i
applyModel()
layer:Destroy()
openPage("Providers")
end)
end
local orItem = button({ Size = UDim2.new(1, 0, 0, 46), BackgroundColor3 = C.hover,
BackgroundTransparency = 1, LayoutOrder = #MODELS + 1, ZIndex = 82 }, list)
round(orItem, 8)
hover(orItem, C.hover, 0.1)
local orDot = new("Frame", { Position = UDim2.fromOffset(10, 14), Size = UDim2.fromOffset(18, 18),
BackgroundTransparency = 1, ZIndex = 83 }, orItem)
icon("search", orDot, C.subtext, 16)
text({ Position = UDim2.fromOffset(36, 6), Size = UDim2.new(1, -60, 0, 16),
Text = "OpenRouter models", TextSize = 12, ZIndex = 83 }, orItem)
text({ Position = UDim2.fromOffset(36, 23), Size = UDim2.new(1, -60, 0, 14),
Text = "Search the live catalogue", TextSize = 10, TextColor3 = C.subtext, ZIndex = 83 }, orItem)
orItem.MouseButton1Click:Connect(function()
layer:Destroy()
openPage("Providers")
openOpenRouterPicker()
end)
backdrop.MouseButton1Click:Connect(function() layer:Destroy() end)
end
local function keyField(card, title, desc, key)
local r = row(card, title, desc, 98)
local state = pill(r, (settings[key] or "") ~= "" and "Set" or "Empty",
(settings[key] or "") ~= "" and Color3.fromRGB(106, 200, 145) or C.subtext, 12)
local stateLabel = state:FindFirstChildOfClass("TextLabel")
local box = new("TextBox", { Position = UDim2.fromOffset(0, 47), Size = UDim2.new(1, -196, 0, 32),
BackgroundColor3 = C.panel, Text = settings[key] or "", PlaceholderText = "Paste " .. title:lower(),
PlaceholderColor3 = C.subtext, TextColor3 = C.text, TextSize = 12, Font = Enum.Font.Code,
ClearTextOnFocus = false, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 64 }, r)
round(box, 8)
stroke(box, C.stroke)
pad(box, 0, 10, 0, 10)
box:GetPropertyChangedSignal("Text"):Connect(function()
settings[key] = box.Text:match("^%s*(.-)%s*$") or ""
if stateLabel and stateLabel.Parent then
local filled = settings[key] ~= ""
stateLabel.Text = filled and "Set" or "Empty"
stateLabel.TextColor3 = filled and Color3.fromRGB(106, 200, 145) or C.subtext
end
end)
local test = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 47),
Size = UDim2.fromOffset(88, 32), BackgroundColor3 = C.hover, Text = "Test", TextSize = 12,
ZIndex = 64 }, r)
round(test, 8)
stroke(test, C.stroke)
hover(test, C.panel, 0)
local clear = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -96, 0, 47),
Size = UDim2.fromOffset(84, 32), BackgroundColor3 = C.hover, Text = "Clear", TextSize = 12,
ZIndex = 64 }, r)
round(clear, 8)
stroke(clear, C.stroke)
hover(clear, C.panel, 0)
clear.MouseButton1Click:Connect(function()
box.Text = ""
settings[key] = ""
if stateLabel and stateLabel.Parent then
stateLabel.Text = "Empty"
stateLabel.TextColor3 = C.subtext
end
end)
test.MouseButton1Click:Connect(function()
if not testApiConnection then return end
if stateLabel and stateLabel.Parent then
stateLabel.Text = "Testing…"
stateLabel.TextColor3 = C.subtext
end
test.Text = "Testing…"
test.Active = false
local provider = key == "opencodeApiKey" and "opencode" or (key == "openrouterKey" and "openrouter" or "agentrouter")
local previous = settings.provider
settings.provider = provider
testApiConnection(settings[key], function(ok, message)
if stateLabel and stateLabel.Parent then
local short = tostring(message or (ok and "Works" or "Failed"))
if #short > 34 then short = short:sub(1, 31) .. "..." end
stateLabel.Text = short
stateLabel.TextColor3 = ok and Color3.fromRGB(106, 200, 145) or C.error
end
if test.Parent then
test.Text = "Test"
test.Active = true
end
settings.provider = previous
end, provider)
end)
end
builders.General = function()
pageHead("General", PAGE_INFO.General.desc)
local session = section("Session")
local gameRow = row(session, "Detected game", "Sent as context with every request")
pill(gameRow, gameName, C.subtext, 19)
toggle(row(session, "Auto compact", "Fold older turns into a summary when a session gets long"),
settings.autoCompact ~= false, function(v) settings.autoCompact = v end)
local startup = section("Startup")
toggle(row(startup, "Logo follows model", "Show the selected model's mark in the sidebar"),
settings.autoLogo ~= false, function(v)
settings.autoLogo = v
applyModel()
end)
end
builders.Personality = function()
pageHead("Personality", PAGE_INFO.Personality.desc)
local card = section("Custom instructions")
local r = row(card, "Personality", "Added to the system message of every conversation", 176)
local box = new("TextBox", { Position = UDim2.fromOffset(0, 47), Size = UDim2.new(1, -110, 0, 92),
BackgroundColor3 = C.panel, Text = settings.personality or "",
PlaceholderText = "Example: Be concise, explain the fix before the code.",
PlaceholderColor3 = C.subtext, TextColor3 = C.text, Font = Enum.Font.Gotham, TextSize = 12,
TextWrapped = true, TextYAlignment = Enum.TextYAlignment.Top,
TextXAlignment = Enum.TextXAlignment.Left, ClearTextOnFocus = false, MultiLine = true,
ZIndex = 64 }, r)
round(box, 8)
stroke(box, C.stroke)
pad(box, 8, 10, 8, 10)
local counter = text({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 47),
Size = UDim2.fromOffset(96, 16), Text = tostring(#(settings.personality or "")) .. " chars",
TextSize = 10, TextColor3 = C.subtext, TextXAlignment = Enum.TextXAlignment.Right,
TextWrapped = false, ZIndex = 64 }, r)
local clearBtn = button({ AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 70),
Size = UDim2.fromOffset(96, 30), BackgroundColor3 = C.hover, Text = "Clear", TextSize = 11,
ZIndex = 64 }, r)
round(clearBtn, 8)
hover(clearBtn, C.panel, 0)
clearBtn.MouseButton1Click:Connect(function()
box.Text = ""
settings.personality = ""
counter.Text = "0 chars"
end)
box:GetPropertyChangedSignal("Text"):Connect(function()
settings.personality = box.Text
if counter.Parent then counter.Text = tostring(#box.Text) .. " chars" end
end)
local presets = section("Starting points")
local holder = new("Frame", { Size = UDim2.new(1, 0, 0, 52), BackgroundTransparency = 1,
LayoutOrder = cardCount[presets] + 1 }, presets)
pad(holder, 12, 14, 12, 14)
new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 6),
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder }, holder)
for i, preset in ipairs({
{ "Concise", "Be concise. Lead with the answer, then the details." },
{ "Explain code", "Explain code changes before showing them." },
{ "Ask first", "Ask before guessing when the request is ambiguous." },
{ "Roblox focus", "Prefer idiomatic Roblox Luau and mention performance costs." },
}) do
local chip = button({ Size = UDim2.fromOffset(0, 28), AutomaticSize = Enum.AutomaticSize.X,
BackgroundColor3 = C.hover, Text = preset[1], TextSize = 11, LayoutOrder = i,
ZIndex = 64 }, holder)
round(chip, 7)
pad(chip, 0, 12, 0, 12)
hover(chip, C.panel, 0)
chip.MouseButton1Click:Connect(function()
local current = settings.personality or ""
local addition = preset[2]
if current ~= "" then addition = current .. " " .. addition end
settings.personality = addition
box.Text = addition
end)
end
end
builders.Providers = function()
pageHead("Providers", PAGE_INFO.Providers.desc)
local routing = section("Routing")
local routingNames = { "OpenCode", "AgentRouter", "OpenRouter" }
local routingIds = { "opencode", "agentrouter", "openrouter" }
segmented(row(routing, "Provider", "Where model requests are sent"),
routingNames, math.max(1, indexOf(routingIds, settings.provider)), function(i)
settings.provider = routingIds[i]
if sideProviderLabel then
sideProviderLabel.Text = providerLabel(MODELS[modelIndex])
end
end)
local modelRow = row(routing, "Active model", "Chosen model for this session")
local picker = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(196, 34), BackgroundColor3 = C.panel, ZIndex = 64 }, modelRow)
round(picker, 8)
stroke(picker, C.stroke)
hover(picker, C.hover, 0)
local dot = new("Frame", { Position = UDim2.fromOffset(10, 9), Size = UDim2.fromOffset(16, 16),
BackgroundColor3 = MODELS[modelIndex].accent, BorderSizePixel = 0, ZIndex = 65 }, picker)
round(dot, 8)
text({ Position = UDim2.fromOffset(34, 7), Size = UDim2.new(1, -44, 0, 20),
Text = MODELS[modelIndex].brandName .. " " .. MODELS[modelIndex].short, TextSize = 12,
TextWrapped = false, TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 65 }, picker)
picker.MouseButton1Click:Connect(openModelPicker)
local keys = section("Credentials")
keyField(keys, "OpenCode key", "Optional. Free models use the shared free pool.", "opencodeApiKey")
keyField(keys, "AgentRouter key", "Required for Claude and GPT models.", "apiKey")
keyField(keys, "OpenRouter key", "Unlocks the full OpenRouter catalogue.", "openrouterKey")
for _, prov in ipairs(API.CATALOGUE_ORDER) do
local spec = API.CATALOGUES[prov]
local browseRow = row(keys, spec.title, "Browse this provider's live catalogue")
local browseBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(110, 30), BackgroundColor3 = C.hover, Text = "Browse", TextSize = 11,
ZIndex = 64 }, browseRow)
round(browseBtn, 8)
stroke(browseBtn, C.stroke)
hover(browseBtn, C.panel, 0)
browseBtn.MouseButton1Click:Connect(function() openCataloguePicker(prov) end)
end
local status = section("Status")
local statusRow = row(status, "Routing preference", "Falls back automatically when a provider refuses")
local routingFirst = { opencode = "Zen first", agentrouter = "AgentRouter first", openrouter = "OpenRouter first" }
pill(statusRow, routingFirst[settings.provider] or "Zen first", C.subtext, 18)
end
builders.Appearance = function()
pageHead("Appearance", PAGE_INFO.Appearance.desc)
themeGrid(section("Theme"))
local motion = section("Motion")
segmented(row(motion, "Transitions", "How quickly panels, tabs and menus move"),
{ "Full", "Subtle", "Off" }, indexOf({ "Full", "Subtle", "Off" }, settings.motion), function(i)
settings.motion = ({ "Full", "Subtle", "Off" })[i]
MOTION = settings.motion == "Off" and 0 or (settings.motion == "Subtle" and 0.6 or 1)
end)
local layoutSection = section("Layout")
segmented(row(layoutSection, "Interface scale", "Zoom the window contents"),
{ "85%", "100%", "110%" }, indexOf({ 0.85, 1, 1.1 }, settings.uiScale), function(i)
settings.uiScale = ({ 0.85, 1, 1.1 })[i]
winScale.Scale = settings.uiScale
end)
segmented(row(layoutSection, "Transcript width", "How wide assistant replies can grow"),
{ "Narrow", "Wide" }, (settings.transcriptWidth or 740) >= 860 and 2 or 1, function(i)
settings.transcriptWidth = i == 2 and 880 or 740
local constraint = column:FindFirstChildOfClass("UISizeConstraint")
if constraint then
constraint.MaxSize = Vector2.new(settings.transcriptWidth, math.huge)
end
end)
end
builders.Permissions = function()
pageHead("Permissions", PAGE_INFO.Permissions.desc)
local access = section("Data access")
toggle(row(access, "Console access", "Read output the game logs"), settings.console ~= false,
function(v) settings.console = v end)
toggle(row(access, "Game vision", "Capture the screen for visual context"), settings.screen ~= false,
function(v) settings.screen = v end)
toggle(row(access, "Web search", "Let the model look things up online"), settings.web ~= false,
function(v) settings.web = v end)
local modes = section("Tool permissions")
for _, mode in ipairs(PERMISSION_MODES) do
optionRow(modes, mode.title, mode.desc, settings.permission == mode.id, function()
settings.permission = mode.id
openPage("Permissions")
end)
end
local note = section("How prompts work")
row(note, "Approvals time out", "A prompt left unanswered for 30 seconds counts as declined.")
end
builders["API keys"] = function()
pageHead("API keys", PAGE_INFO["API keys"].desc)
local keys = section("Credentials")
keyField(keys, "AgentRouter key", "Required for Claude and GPT models.", "apiKey")
keyField(keys, "OpenCode key", "Optional. Free models use the shared free pool.", "opencodeApiKey")
keyField(keys, "OpenRouter key", "Unlocks the full OpenRouter catalogue.", "openrouterKey")
local storage = section("Storage")
local fileRow = row(storage, "Settings file", "Keys and preferences are written here only")
pill(fileRow, "AgentRouterRobloxAI.json", C.subtext, 18)
row(storage, "Plain text", "Treat this file like a password. Nothing is uploaded.")
local danger = section("Reset")
local clearRow = row(danger, "Remove all keys", "Clears the fields above and saves an empty key")
local clearBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, 0, 0.5, 0),
Size = UDim2.fromOffset(120, 30), BackgroundColor3 = C.hover, Text = "Remove keys",
TextSize = 11, ZIndex = 64 }, clearRow)
round(clearBtn, 8)
stroke(clearBtn, C.stroke)
hover(clearBtn, C.error, 0.75)
local armed = false
clearBtn.MouseButton1Click:Connect(function()
if not armed then
armed = true
clearBtn.Text = "Click to confirm"
task.delay(3, function()
if clearBtn.Parent then
armed = false
clearBtn.Text = "Remove keys"
end
end)
return
end
settings.apiKey = ""
settings.opencodeApiKey = ""
settings.openrouterKey = ""
AGENTROUTER_KEY, OPENCODE_KEY = "", ""
saveSettings()
openPage("API keys")
end)
end
local function showResults(query)
clearContent()
local matches = {}
for _, entry in ipairs(SEARCH) do
if entry.title:lower():find(query, 1, true) or entry.desc:lower():find(query, 1, true) then
matches[#matches + 1] = entry
end
end
pageHead("Search", tostring(#matches) .. " setting" .. (#matches == 1 and "" or "s") .. " found")
local card = section("Results")
if #matches == 0 then
row(card, "Nothing matches", "Try a shorter word, like theme, key or motion.")
return
end
for _, entry in ipairs(matches) do
cardCount[card] = (cardCount[card] or 0) + 1
local index = cardCount[card]
local r = button({ Size = UDim2.new(1, 0, 0, 52), BackgroundTransparency = 1,
LayoutOrder = index, ZIndex = 63 }, card)
pad(r, 0, 14, 0, 14)
if index > 1 then
new("Frame", { Position = UDim2.new(0, 14, 0, 0), Size = UDim2.new(1, -28, 0, 1),
BackgroundColor3 = C.stroke, BackgroundTransparency = 0.5,
BorderSizePixel = 0 }, r)
end
text({ Position = UDim2.fromOffset(0, 9), Size = UDim2.new(1, -110, 0, 18), Text = entry.title,
TextSize = 13, ZIndex = 64 }, r)
text({ Position = UDim2.fromOffset(0, 27), Size = UDim2.new(1, -110, 0, 16), Text = entry.desc,
TextSize = 10, TextColor3 = C.subtext, TextWrapped = false,
TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 64 }, r)
pill(r, entry.page, C.subtext, 15)
r.MouseButton1Click:Connect(function()
suppressSearch = true
navSearch.Text = ""
suppressSearch = false
openPage(entry.page)
end)
end
end
openPage = function(name)
if not builders[name] then name = "General" end
currentPage = name
for pageName, entry in pairs(navButtons) do
local active = pageName == name
tween(entry.btn, 0.14, { BackgroundTransparency = active and 0.35 or 1 })
entry.label.TextColor3 = active and C.text or C.subtext
entry.bar.Visible = active
end
clearContent()
builders[name]()
content.CanvasPosition = Vector2.new(0, 0)
content.Position = UDim2.fromOffset(234, 78)
tween(content, 0.16, { Position = UDim2.fromOffset(228, 78) })
end
local navY, lastGroup = 2, nil
for _, page in ipairs(PAGES) do
if page.group ~= lastGroup then
lastGroup = page.group
text({ Position = UDim2.fromOffset(10, navY + 6), Size = UDim2.new(1, -20, 0, 12),
Text = page.group:upper(), Font = Enum.Font.GothamBold, TextSize = 9,
TextColor3 = C.subtext, TextWrapped = false, ZIndex = 64 }, navScroll)
navY = navY + 24
end
local b = button({ Position = UDim2.fromOffset(0, navY), Size = UDim2.new(1, 0, 0, 32),
BackgroundColor3 = C.hover, BackgroundTransparency = 1, ZIndex = 63 }, navScroll)
round(b, 8)
hover(b, C.hover, 0.35)
local bar = new("Frame", { Position = UDim2.fromOffset(0, 9), Size = UDim2.fromOffset(2, 14),
BackgroundColor3 = accent, BorderSizePixel = 0, Visible = false, ZIndex = 65 }, b)
round(bar, 1)
local ic = new("Frame", { BackgroundTransparency = 1, Position = UDim2.fromOffset(12, 8),
Size = UDim2.fromOffset(16, 16), ZIndex = 64 }, b)
icon(page.icon, ic, C.subtext, 16)
local label = text({ Position = UDim2.fromOffset(38, 0), Size = UDim2.new(1, -48, 1, 0),
Text = page.id, TextSize = 12, TextColor3 = C.subtext, TextWrapped = false,
ZIndex = 64 }, b)
navButtons[page.id] = { btn = b, label = label, bar = bar }
b.MouseButton1Click:Connect(function()
suppressSearch = true
navSearch.Text = ""
suppressSearch = false
openPage(page.id)
end)
navY = navY + 35
end
navScroll.CanvasSize = UDim2.new(0, 0, 0, navY + 6)
local function applySearch(raw)
local query = tostring(raw or ""):lower():match("^%s*(.-)%s*$")
for pageName, entry in pairs(navButtons) do
local visible = query == ""
if not visible then
visible = pageName:lower():find(query, 1, true) ~= nil
end
if not visible then
for _, item in ipairs(SEARCH) do
if item.page == pageName and (item.title:lower():find(query, 1, true)
or item.desc:lower():find(query, 1, true)) then
visible = true
break
end
end
end
entry.btn.Visible = visible
end
if query == "" then
openPage(currentPage)
else
showResults(query)
end
end
navSearch:GetPropertyChangedSignal("Text"):Connect(function()
if suppressSearch then return end
applySearch(navSearch.Text)
end)
text({ Position = UDim2.new(0, 20, 1, -60), Size = UDim2.new(0, 216, 0, 60),
Text = "Stored locally · AgentRouterRobloxAI.json", TextSize = 10,
TextColor3 = C.subtext, TextWrapped = false, ZIndex = 64 }, panel)
local saveNote = text({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -360, 1, -30),
Size = UDim2.fromOffset(100, 16), Text = "", TextSize = 10, TextColor3 = C.subtext,
TextXAlignment = Enum.TextXAlignment.Right, TextWrapped = false, ZIndex = 64 }, panel)
local resetBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -256, 1, -30),
Size = UDim2.fromOffset(96, 34), BackgroundColor3 = C.hover, Text = "Reset all",
TextSize = 12, ZIndex = 64 }, panel)
round(resetBtn, 9)
stroke(resetBtn, C.stroke)
hover(resetBtn, C.panel, 0)
local saveBtn = button({ AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -20, 1, -30),
Size = UDim2.fromOffset(132, 34), BackgroundColor3 = accent, Text = "Save changes",
TextSize = 13, ZIndex = 64 }, panel)
round(saveBtn, 9)
hover(saveBtn, accent, 0.12)
local function applySettings()
AGENTROUTER_KEY = settings.apiKey or ""
OPENCODE_KEY = settings.opencodeApiKey or ""
winScale.Scale = settings.uiScale or 1
local constraint = column:FindFirstChildOfClass("UISizeConstraint")
if constraint then
constraint.MaxSize = Vector2.new(settings.transcriptWidth or 740, math.huge)
end
MOTION = settings.motion == "Off" and 0 or (settings.motion == "Subtle" and 0.6 or 1)
applyTheme(settings.theme)
return saveSettings()
end
local function saveNow()
local saved = applySettings()
if API.settingsUI then API.settingsUI.lastSave = saved and "Saved" or "Applied" end
saveNote.Text = saved and "Saved" or "Applied"
saveNote.TextColor3 = saved and Color3.fromRGB(106, 200, 145) or C.subtext
task.delay(2, function() if saveNote.Parent then saveNote.Text = "" end end)
end
local function resetNow()
settings.theme = "Claude"
settings.motion = "Full"
settings.uiScale = 1
settings.transcriptWidth = 740
settings.autoLogo = true
settings.autoCompact = true
settings.permission = "ask"
settings.console = true
settings.screen = true
settings.web = true
settings.personality = ""
winScale.Scale = 1
MOTION = 1
local constraint = column:FindFirstChildOfClass("UISizeConstraint")
if constraint then constraint.MaxSize = Vector2.new(740, math.huge) end
applyTheme("Claude")
saveSettings()
openPage(currentPage)
saveNote.Text = "Defaults restored"
saveNote.TextColor3 = Color3.fromRGB(106, 200, 145)
task.delay(2, function() if saveNote.Parent then saveNote.Text = "" end end)
end
local resetArmed = false
resetBtn.MouseButton1Click:Connect(function()
if not resetArmed then
resetArmed = true
resetBtn.Text = "Confirm reset"
task.delay(3, function()
if resetBtn.Parent then
resetArmed = false
resetBtn.Text = "Reset all"
end
end)
return
end
resetArmed = false
resetBtn.Text = "Reset all"
resetNow()
end)
saveBtn.MouseButton1Click:Connect(saveNow)
API.settingsUI = { openPage = function(name) openPage(name) end,
search = function(value)
suppressSearch = true
navSearch.Text = tostring(value or "")
suppressSearch = false
applySearch(value)
end,
save = saveNow, reset = resetNow, close = closePanel, panel = panel,
isOpen = function() return settingsPanel == overlay end }
pcall(function()
keyConn = UserInputService.InputBegan:Connect(function(input, processed)
if processed then return end
if input.KeyCode == Enum.KeyCode.Escape and settingsPanel == overlay then
closePanel()
end
end)
end)
openPage(initialTab or "General")
end
-- Measure the wrapped text ourselves. TextBox.TextBounds is unreliable while the box is
-- being resized, which made long messages scroll sideways instead of growing downwards.
function measureInputHeight()
-- AbsoluteSize includes UIScale; convert back to the unscaled units used by Size.
local scale = (winScale and winScale.Scale) or 1
if scale <= 0 then scale = 1 end
local width = input.AbsoluteSize.X / scale
if width < 80 then
width = math.max(120, composer.AbsoluteSize.X / scale - 32)
end
local probe = input.Text
if probe == "" then probe = "Ag" end
local ok, bounds = pcall(function()
return game:GetService("TextService"):GetTextSize(probe, 15, Enum.Font.Gotham, Vector2.new(width, 100000))
end)
local textH = (ok and bounds and bounds.Y) or 15
local winH = (window.AbsoluteSize.Y > 0 and window.AbsoluteSize.Y or 600) / scale
local maxH = math.clamp(winH * 0.42, 96, 220)
return math.clamp(math.ceil(textH) + 4, 24, maxH)
end
function updateComposer()
local h = measureInputHeight()
local ah = #attachments > 0 and 70 or 0
attachRow.Visible = ah > 0
input.Position = UDim2.fromOffset(16, 14 + ah)
input.Size = UDim2.new(1, -32, 0, h)
local ch = h + 68 + ah
composer.Size = UDim2.new(1, -32, 0, ch)
messageArea.Size = UDim2.new(1, 0, 1, -(52 + ch + 24))
local canSend = busy or input.Text:match("%S") ~= nil or #attachments > 0
tween(sendBtn, 0.12, { BackgroundTransparency = canSend and 0 or 0.65 })
arrowIcon.Visible = not busy
stopIcon.Visible = busy
end
input:GetPropertyChangedSignal("Text"):Connect(function()
if input.Text:sub(-1) == "\n" and not UserInputService:IsKeyDown(Enum.KeyCode.LeftShift) then
input.Text = input.Text:sub(1, -2)
if not busy then send() end
return
end
updateComposer()
end)
searchBox:GetPropertyChangedSignal("Text"):Connect(refreshSidebar)
-- AgentRouter fingerprints the client. Codex models accept the Codex CLI signature,
-- but Claude models are only accepted from a Claude Code client.
API.CODEX_SPOOF = {
["Originator"] = "codex_cli_rs",
["User-Agent"] = "codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464",
["Version"] = "0.101.0",
}
API.CLAUDE_SPOOF = {
["User-Agent"] = "claude-cli/2.1.119 (external, cli)",
["x-app"] = "cli",
["X-Stainless-Arch"] = "x64",
["X-Stainless-Lang"] = "js",
["X-Stainless-OS"] = "Linux",
["X-Stainless-Package-Version"] = "0.60.0",
["X-Stainless-Runtime"] = "node",
["X-Stainless-Runtime-Version"] = "v22.14.0",
["anthropic-beta"] = "interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,claude-code-20250219",
["anthropic-dangerous-direct-browser-access"] = "true",
}
-- OpenCode Zen's free tier is gated on a client fingerprint, not on an account:
-- 1) User-Agent whose leading token is opencode/<version>
-- 2) an x-opencode-session shaped ses_<12 lowercase hex><14 alnum>
-- 3) stream:true in the body
-- 4) at least five of OpenCode's own tool names in tools[] (bash/edit/glob/grep/read)
-- The free models accept "Authorization: Bearer public", so no key is required.
API.OPENCODE_UA = "opencode/1.18.31 ai-sdk/provider-utils/4.0.38 runtime/bun/1.3.14"
function newOpencodeSession()
local hex = "0123456789abcdef"
local alnum = "abcdefghijklmnopqrstuvwxyz0123456789"
local a, b = {}, {}
for _ = 1, 12 do local i = math.random(1, #hex) a[#a + 1] = hex:sub(i, i) end
for _ = 1, 14 do local i = math.random(1, #alnum) b[#b + 1] = alnum:sub(i, i) end
return "ses_" .. table.concat(a) .. table.concat(b)
end
API.OPENCODE_TOOLS = {
{ type = "function", ["function"] = { name = "bash", description = "Run a shell command.", parameters = { type = "object", properties = { command = { type = "string" } }, required = { "command" } } } },
{ type = "function", ["function"] = { name = "edit", description = "Edit a file.", parameters = { type = "object", properties = { filePath = { type = "string" }, oldString = { type = "string" }, newString = { type = "string" } }, required = { "filePath" } } } },
{ type = "function", ["function"] = { name = "glob", description = "Find files by pattern.", parameters = { type = "object", properties = { pattern = { type = "string" } }, required = { "pattern" } } } },
{ type = "function", ["function"] = { name = "grep", description = "Search file contents.", parameters = { type = "object", properties = { pattern = { type = "string" } }, required = { "pattern" } } } },
{ type = "function", ["function"] = { name = "read", description = "Read a file.", parameters = { type = "object", properties = { filePath = { type = "string" } }, required = { "filePath" } } } },
}
local function opencodeSpoof(key)
return {
["User-Agent"] = API.OPENCODE_UA,
["Accept"] = "text/event-stream",
["x-opencode-session"] = newOpencodeSession(),
["x-opencode-client"] = "cli",
["Authorization"] = "Bearer " .. ((key ~= nil and key ~= "") and key or "public"),
}
end
-- Walk a Server-Sent-Events body and stitch the streamed deltas back together.
function parseOpencodeStream(raw)
local text, thinking = {}, {}
local calls = {}
local ok0, decoded0 = pcall(function() return HttpService:JSONDecode(raw) end)
if ok0 and type(decoded0) == "table" and decoded0.choices then
return nil
end
for line in tostring(raw):gmatch("[^\r\n]+") do
local data = line:match("^data:%s*(.+)$")
if data and data ~= "[DONE]" then
local ok, chunk = pcall(function() return HttpService:JSONDecode(data) end)
if ok and type(chunk) == "table" then
if type(chunk.error) == "table" and chunk.error.message then
error("OpenCode: " .. tostring(chunk.error.message))
end
local choice = chunk.choices and chunk.choices[1]
local delta = choice and choice.delta
if type(delta) == "table" then
if type(delta.content) == "string" then table.insert(text, delta.content) end
if type(delta.reasoning) == "string" then table.insert(thinking, delta.reasoning) end
if type(delta.reasoning_content) == "string" then table.insert(thinking, delta.reasoning_content) end
if type(delta.tool_calls) == "table" then
for _, tc in ipairs(delta.tool_calls) do
if type(tc) == "table" then
local idx = #calls + 1
if tonumber(tc.index) ~= nil then idx = tonumber(tc.index) + 1 end
calls[idx] = calls[idx] or { id = "", name = "", args = "" }
if tc.id then calls[idx].id = tc.id end
local fn = tc["function"]
if type(fn) == "table" then
if fn.name then calls[idx].name = fn.name end
if type(fn.arguments) == "string" then calls[idx].args = calls[idx].args .. fn.arguments end
end
end
end
end
end
end
end
end
local answer = table.concat(text)
local reason = table.concat(thinking)
local toolCalls = nil
if #calls > 0 then
toolCalls = {}
for _, c in ipairs(calls) do
if c.name ~= "" then
local args = {}
if c.args ~= "" then
local okA, dec = pcall(function() return HttpService:JSONDecode(c.args) end)
if okA and type(dec) == "table" then args = dec end
end
local id = c.id ~= "" and c.id or ("call_" .. tostring(#toolCalls + 1))
table.insert(toolCalls, { id = id, name = c.name, args = args })
end
end
if #toolCalls == 0 then toolCalls = nil end
end
if toolCalls then
return { text = answer, thinking = reason ~= "" and reason or nil, searches = {}, toolCalls = toolCalls }
end
if answer == "" and reason ~= "" then
return { text = reason, thinking = nil, searches = {} }
end
if answer == "" then error("The free model returned an empty stream. Try another free model.") end
return { text = answer, thinking = reason ~= "" and reason or nil, searches = {} }
end
local function requestHttp(url, method, headers, body, spoof)
local fn = (syn and syn.request) or http_request or request
if type(fn) ~= "function" then
error("This executor has no HTTP request function. Use an executor that supports request/http_request.")
end
local merged = {}
for k, v in pairs(spoof or API.CODEX_SPOOF) do merged[k] = v end
for k, v in pairs(headers or {}) do merged[k] = v end
local ok, response, extra = pcall(fn, {
Url = url,
Method = method,
Headers = merged,
Body = body or "",
Timeout = 90,
})
if not ok then error("Executor HTTP failure: " .. tostring(response):sub(1, 350)) end
if type(response) ~= "table" then
local wrapped
pcall(function() wrapped = { StatusCode = response.StatusCode, Body = response.Body } end)
if type(wrapped) == "table" and (wrapped.StatusCode ~= nil or wrapped.Body ~= nil) then
response = wrapped
elseif type(response) == "string" then
response = { StatusCode = 200, Body = response }
else
local detail = type(response) .. ": " .. tostring(response):sub(1, 120)
if extra ~= nil then detail = detail .. " | " .. tostring(extra):sub(1, 160) end
error("Executor returned an unreadable HTTP response (" .. detail .. ")")
end
end
local status = tonumber(response.StatusCode or response.Status or response.status_code or response.status) or 0
local responseBody = response.Body or response.body or response.response or ""
if status == 0 and tostring(responseBody) ~= "" then status = 200 end
if status < 200 or status >= 300 then
local detail = tostring(responseBody):gsub("[%c]", " ")
local okDecode, decoded = pcall(function() return HttpService:JSONDecode(responseBody) end)
if okDecode and type(decoded) == "table" then
local err = decoded.error
if type(err) == "table" then detail = tostring(err.message or err.msg or err.code or responseBody)
elseif err ~= nil then detail = tostring(err)
elseif decoded.msg ~= nil then detail = tostring(decoded.msg)
elseif decoded.message ~= nil then detail = tostring(decoded.message) end
end
if detail == "" then detail = "(empty response body)" end
local freeLimit = detail:lower()
if freeLimit:find("free usage limit", 1, true) or freeLimit:find("freeusagelimit", 1, true)
or freeLimit:find("only be used from within opencode", 1, true) or freeLimit:find("freetiererror", 1, true) then
error("OPENCODE_FREE_TIER:" .. detail:sub(1, 200))
end
if status == 401 then
error("Authentication failed (HTTP 401). Check your API key for the selected provider. Response: " .. detail:sub(1, 350))
end
if status == 402 then
if url:find("openrouter.ai", 1, true) then
error("HTTP 402: OpenRouter says this request is not covered by your credits. Response: " .. detail:sub(1, 250))
end
error("HTTP 402: AgentRouter's premium-model batch quota is empty. This is separate from your account balance/credits. "
.. "Claude Opus 5.5 and GPT-6.x refill daily at 10:00 and 19:00 Beijing time. DeepSeek V4 Flash is unlimited.")
end
if status == 403 then
local dl = detail:lower()
if dl:find("freetier") or dl:find("free tier") or dl:find("within opencode") then
error("OPENCODE_FREE_TIER")
end
if dl:find("unauthorized client") then
error("AgentRouter rejected this client's fingerprint (unauthorized_client_error). Claude models require the Claude Code headers; other models require the Codex headers.")
end
error("Access denied (HTTP 403). " .. detail:sub(1, 200))
end
if status == 400 then
local dl = detail:lower()
if dl:find("content%-blocked") or dl:find("content_blocked") or dl:find("content blocked")
or dl:find("blocked by the provider") or dl:find("content filter") or dl:find("sensitive content") then
error("CONTENT_BLOCKED|" .. detail:sub(1, 200))
end
if dl:find("/v1/responses") or dl:find("responses") then
error("NEED_RESPONSES_API:" .. detail:sub(1, 200))
end
error("Bad request (HTTP 400): " .. detail:sub(1, 250))
end
if status == 404 then
error("Not found (HTTP 404). Endpoint or model ID is invalid. Response: " .. detail:sub(1, 350))
end
if status == 429 then
error("Rate limited (HTTP 429). Wait and retry, or switch model. Response: " .. detail:sub(1, 300))
end
if status >= 500 then
local dl = detail:lower()
if dl:find("无可用渠道", 1, true) or dl:find("no available channel", 1, true)
or dl:find("no channel available", 1, true) then
error("NO_CHANNEL|" .. detail:sub(1, 300))
end
if dl:find("sensitive words") or dl:find("sensitive content") or dl:find("content_blocked") then
error("CONTENT_BLOCKED|" .. detail:sub(1, 200))
end
error("Server error (HTTP " .. tostring(status) .. "). Temporary failure — retry later. Response: " .. detail:sub(1, 300))
end
error("HTTP " .. tostring(status) .. ": " .. detail:sub(1, 500))
end
return responseBody, status
end
API.modelCache = { at = 0, ids = nil, missAt = 0 }
API.blockedModels = {}
API.lastGoodModel = nil
API.agentRouterModels = function(key)
local now = os.time()
if API.modelCache.ids and now - API.modelCache.at < 300 then return API.modelCache.ids end
if not API.modelCache.ids and now - (API.modelCache.missAt or 0) < 60 then return nil end
if key == nil or key == "" then return nil end
local ok, raw = pcall(function()
return requestHttp("https://agentrouter.org/v1/models", "GET", {
["Accept"] = "application/json",
["Authorization"] = "Bearer " .. key,
})
end)
if not ok then API.modelCache.missAt = now return nil end
local okDecode, decoded = pcall(function() return HttpService:JSONDecode(raw) end)
if not okDecode or type(decoded) ~= "table" then API.modelCache.missAt = now return nil end
local ids = {}
for _, item in ipairs(decoded.data or decoded.models or {}) do
local id = type(item) == "table" and (item.id or item.model or item.name) or nil
if type(id) == "string" and id ~= "" then ids[#ids + 1] = id end
end
if #ids == 0 then return nil end
API.modelCache.at, API.modelCache.ids = now, ids
return ids
end
API.preferredModel = function(ids)
if type(ids) ~= "table" or #ids == 0 then return nil end
local have = {}
for _, id in ipairs(ids) do have[id] = true end
for _, id in ipairs({ "gpt-6.1-sol", "gpt-6-astra", "gpt-5.6-sol" }) do
if have[id] then return id end
end
for _, m in ipairs(MODELS) do
if m.format == "openai" and have[m.id] then return m.id end
end
for _, id in ipairs(ids) do
local lower = id:lower()
if not lower:find("embed") and not lower:find("whisper") and not lower:find("tts")
and not lower:find("image") and not lower:find("dall") and not lower:find("moderation") then
return id
end
end
return nil
end
do
local BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
function urlEncode(s)
return (tostring(s):gsub("[^%w]", function(c) return string.format("%%%02X", string.byte(c)) end))
end
function urlDecode(s)
return (tostring(s):gsub("%%(%x%x)", function(h) return string.char(tonumber(h, 16)) end))
end
local function decodeEntities(s)
s = s:gsub("<", "<"):gsub(">", ">"):gsub(""", '"'):gsub("'", "'"):gsub("'", "'")
s = s:gsub(" ", " ")
return (s:gsub("&", "&"))
end
local BLOCK_TAGS = { p = true, div = true, li = true, ul = true, ol = true, dl = true, dt = true, dd = true,
h1 = true, h2 = true, h3 = true, h4 = true, h5 = true, h6 = true, br = true, hr = true,
tr = true, td = true, th = true, table = true, thead = true, tbody = true, tfoot = true, caption = true,
section = true, article = true, header = true, footer = true, main = true, nav = true, aside = true,
blockquote = true, pre = true, figure = true, figcaption = true, form = true, fieldset = true,
legend = true, address = true, details = true, summary = true, center = true, colgroup = true }
function stripHtml(s)
local SC = "[Ss][Cc][Rr][Ii][Pp][Tt]"
local ST = "[Ss][Tt][Yy][Ll][Ee]"
s = s:gsub("<" .. SC .. "[^>]*>.-</" .. SC .. "[%s>]", " ")
s = s:gsub("<" .. ST .. "[^>]*>.-</" .. ST .. "[%s>]", " ")
s = s:gsub("<!%-%-.-%-%->", " ")
s = s:gsub("<[^>]+>", function(tag)
local name = tag:match("^</?[%a][%w%d%-]*")
if name then
name = name:lower():gsub("^</?", "")
if BLOCK_TAGS[name] then return "\n" end
end
return " "
end)
s = decodeEntities(s)
s = s:gsub("[%c]", function(ch) if ch == "\n" or ch == "\t" then return ch end return " " end)
s = s:gsub("[ \t]+", " ")
s = s:gsub(" *\n *", "\n")
s = s:gsub("\n\n\n+", "\n\n")
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
webSearch = function(query)
local ok, body = pcall(requestHttp,
"https://html.duckduckgo.com/html/?q=" .. urlEncode(query), "GET",
{ ["User-Agent"] = BROWSER_UA, ["Accept"] = "text/html,application/xhtml+xml" }, "", {})
if not ok then return nil, tostring(body):sub(1, 200) end
if type(body) ~= "string" or body == "" then return nil, "empty response" end
local results, pos = {}, 1
while #results < 8 do
local s, e, href, title = body:find('class="result__a" href="([^"]+)">([^<]*)</a>', pos)
if not s then break end
href = href:gsub("&", "&")
local u = href:match("uddg=([^&]+)")
u = u and urlDecode(u) or nil
if u and u:match("^https?://") then
local snip = ""
local snipS, _, rawSnip = body:find('class="result__snippet"[^>]*>(.-)</a>', e + 1)
if snipS and rawSnip then snip = decodeEntities((rawSnip:gsub("<[^>]+>", ""))) end
results[#results + 1] = { url = u, title = decodeEntities(title), snippet = snip }
end
pos = e + 1
end
if #results == 0 then return nil, "no results (page blocked or format changed)" end
local lines = {}
for i, r in ipairs(results) do
lines[#lines + 1] = i .. ". " .. r.title
lines[#lines + 1] = " " .. r.url
if r.snippet ~= "" then lines[#lines + 1] = " " .. r.snippet end
end
return table.concat(lines, "\n")
end
webFetch = function(url)
if not url:match("^https?://") then
if url:match("^%a[%w+.-]*://") then return nil, "only http(s) URLs can be read" end
url = "https://" .. url
end
local ok, body = pcall(requestHttp, url, "GET",
{ ["User-Agent"] = BROWSER_UA, ["Accept"] = "text/html,application/xhtml+xml,text/plain,*/*" }, "", {})
if not ok then return nil, tostring(body):sub(1, 200) end
if type(body) ~= "string" then return nil, "unreadable response" end
local out = stripHtml(body)
if #out > 6000 then
out = out:sub(1, 6000)
while #out > 0 and out:byte(-1) >= 128 do out = out:sub(1, -2) end
out = out .. "\n... (truncated)"
end
if out == "" then return nil, "page had no readable text" end
return out
end
end
local function requestOpenAICompatible(url, payload, headers, spoof)
local candidate = table.clone(payload)
local changed = {}
for attempt = 1, 4 do
local ok, result = pcall(function()
return requestHttp(url, "POST", headers, HttpService:JSONEncode(candidate), spoof)
end)
if ok then return result end
local message = tostring(result)
local lower = message:lower()
local unsupported = lower:find("unsupported", 1, true)
or lower:find("not support", 1, true)
or lower:find("unknown parameter", 1, true)
or lower:find("unrecognized parameter", 1, true)
or lower:find("invalid parameter", 1, true)
or lower:find("invalid value", 1, true)
if not unsupported then error(message) end
if lower:find("tool_choice", 1, true) and candidate.tool_choice ~= nil and not changed.tool_choice then
candidate.tool_choice = nil
changed.tool_choice = true
elseif lower:find("reasoning_effort", 1, true) and candidate.reasoning_effort ~= nil and not changed.reasoning_effort then
candidate.reasoning_effort = nil
changed.reasoning_effort = true
elseif lower:find("max_tokens", 1, true) and candidate.max_tokens ~= nil and not changed.max_tokens then
candidate.max_completion_tokens = candidate.max_tokens
candidate.max_tokens = nil
changed.max_tokens = true
elseif lower:find("max_completion_tokens", 1, true) and candidate.max_completion_tokens ~= nil and not changed.max_completion_tokens then
candidate.max_tokens = candidate.max_completion_tokens
candidate.max_completion_tokens = nil
changed.max_completion_tokens = true
else
if lower:find("model", 1, true) and (lower:find("not supported", 1, true) or lower:find("unsupported", 1, true) or lower:find("not found", 1, true)) then
error(message .. "\nCheck the model ID against models available for your key.")
end
error(message)
end
end
error("Request rejected after compatibility retries.")
end
local function apiKeyValue(value)
local key = tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
key = key:gsub('^"(.*)"$', "%1"):gsub("^'(.*)'$", "%1")
key = key:gsub("^[Bb][Ee][Aa][Rr][Ee][Rr]%s+", "")
return key:gsub("^%s+", ""):gsub("%s+$", "")
end
API.CATALOGUES = {
opencode = {
title = "OpenCode Zen models", blurb = "Search the free Zen catalogue",
url = API.OPENCODE_MODELS_URL, provider = "opencode", spoof = {},
accent = Color3.fromRGB(60, 130, 255), brand = "OpenCode Zen",
missing = "Zen free models still answer; a key unlocks the paid ones.",
},
agentrouter = {
title = "AgentRouter models", blurb = "Search the models this key can reach",
url = API.AGENTROUTER_MODELS_URL, provider = "agentrouter", spoof = API.CODEX_SPOOF,
accent = Color3.fromRGB(77, 107, 254), brand = "AgentRouter",
missing = "Add an AgentRouter key in Settings \u{2192} Providers to load this list.",
},
openrouter = {
title = "OpenRouter models", blurb = "Search the live catalogue",
url = API.OPENROUTER_MODELS_URL, provider = "openrouter", spoof = API.OPENROUTER_SPOOF,
accent = Color3.fromRGB(120, 100, 240), brand = "OpenRouter",
missing = "Add an OpenRouter key in Settings \u{2192} Providers to use these.",
},
}
API.CATALOGUE_ORDER = { "opencode", "agentrouter", "openrouter" }
API.catalogueCache, API.catalogueWaiting, API.catalogueLoading = {}, {}, {}
function API.catalogueKey(provider)
if provider == "openrouter" then return apiKeyValue(settings.openrouterKey) end
if provider == "opencode" then
local key = apiKeyValue(settings.opencodeApiKey)
if key == "" then key = apiKeyValue(OPENCODE_KEY) end
return key
end
local key = apiKeyValue(settings.apiKey)
if key == "" then key = apiKeyValue(AGENTROUTER_KEY) end
return key
end
function API.prettifyId(id)
local tail = tostring(id or ""):match("([^/]+)$") or tostring(id or "")
local words = {}
for word in tail:gmatch("[^%-_]+") do
words[#words + 1] = word:sub(1, 1):upper() .. word:sub(2)
end
local text = table.concat(words, " ")
if text == "" then return tostring(id or "") end
return text
end
function API.parseCatalogue(provider)
local spec = API.CATALOGUES[provider]
if not spec then return nil, "Unknown provider." end
local key = API.catalogueKey(provider)
if provider == "agentrouter" and key == "" then return nil, spec.missing end
local headers = { ["Accept"] = "application/json" }
if key ~= "" then headers["Authorization"] = "Bearer " .. key end
local function fetch(sent, spoof)
local ok, result = pcall(requestHttp, spec.url, "GET", sent, "", spoof or {})
if ok then return result end
return nil, tostring(result)
end
local raw, failure = fetch(headers)
if not raw then
local text = tostring(failure)
if text:find("431", 1, true) or text:find("unreadable HTTP", 1, true) then
local slim = {}
if headers["Authorization"] then slim["Authorization"] = headers["Authorization"] end
local slimRaw, slimFailure = fetch(slim)
if slimRaw then
raw = slimRaw
else
failure = tostring(slimFailure)
end
end
end
if not raw and type(spec.spoof) == "table" and next(spec.spoof) then
local spoofRaw, spoofFailure = fetch(headers, spec.spoof)
if spoofRaw then raw = spoofRaw else failure = tostring(spoofFailure) end
end
if not raw then
local text = tostring(failure or "unknown error")
if text:find("431", 1, true) then
text = "the executor's HTTP layer sent headers the server refused (HTTP 431). Restart the executor and try again; if it keeps failing, this executor cannot load the catalogue."
end
return nil, text
end
local okDecode, decoded = pcall(function() return HttpService:JSONDecode(tostring(raw)) end)
if not okDecode or type(decoded) ~= "table" then return nil, spec.title .. " sent a list this could not read." end
local rows = type(decoded.data) == "table" and decoded.data
or (type(decoded.models) == "table" and decoded.models or nil)
if not rows then return nil, spec.title .. " sent a list this could not read." end
local list = {}
for _, item in ipairs(rows) do
local id = ""
if type(item) == "table" then id = tostring(item.id or item.model or item.name or "") end
if id ~= "" and not (provider == "opencode" and id == "test") then
local price = type(item.pricing) == "table" and item.pricing or {}
local arch = type(item.architecture) == "table" and item.architecture or {}
local inputs = type(arch.input_modalities) == "table" and arch.input_modalities or {}
local vision = false
for _, mode in ipairs(inputs) do
if tostring(mode) == "image" then vision = true break end
end
local free
if price.prompt ~= nil or price.completion ~= nil then
free = tostring(price.prompt or "1") == "0" and tostring(price.completion or "1") == "0"
elseif provider == "opencode" then
free = id:find("free", 1, true) ~= nil
end
local name = item.name or item.display_name
if not name and provider == "opencode" then
name = API.prettifyId((id:gsub("%-free$", "")))
end
if vision == false and id:find("%-vl", 1, true) then vision = true end
list[#list + 1] = { id = id, name = tostring(name or API.prettifyId(id)),
ctx = tonumber(item.context_length or item.context) or 0, vision = vision, free = free }
end
end
if #list == 0 then return nil, spec.title .. " returned no models." end
table.sort(list, function(a, b)
if (a.free == true) ~= (b.free == true) then return a.free == true end
return a.name:lower() < b.name:lower()
end)
return list
end
function API.loadCatalogue(provider, callback)
local cached = API.catalogueCache[provider]
if cached then callback(cached) return end
local waiting = API.catalogueWaiting[provider] or {}
API.catalogueWaiting[provider] = waiting
table.insert(waiting, callback)
if API.catalogueLoading[provider] then return end
API.catalogueLoading[provider] = true
task.spawn(function()
local list, failure = API.parseCatalogue(provider)
API.catalogueLoading[provider] = false
if list then API.catalogueCache[provider] = list end
local queued = API.catalogueWaiting[provider] or {}
API.catalogueWaiting[provider] = {}
for _, fn in ipairs(queued) do pcall(fn, list, failure) end
end)
end
function API.openRouterCatalog(callback) API.loadCatalogue("openrouter", callback) end
function API.catalogueDescriptor(provider, entry)
local spec = API.CATALOGUES[provider] or API.CATALOGUES.openrouter
local id = tostring(entry.id or "")
local name = tostring(entry.name or id)
local short = id:match("([^/]+)$") or name
local desc
if provider == "openrouter" then
if entry.free then
desc = "OpenRouter · free"
elseif (tonumber(entry.ctx) or 0) >= 1000 then
desc = "OpenRouter · " .. tostring(math.floor(tonumber(entry.ctx) / 1000)) .. "k context"
else
desc = "OpenRouter · paid"
end
elseif provider == "opencode" then
desc = entry.free and "OpenCode Zen · free" or "OpenCode Zen"
else
desc = entry.free and "AgentRouter · free" or "AgentRouter"
end
local accent, format = spec.accent, "openai"
if provider == "agentrouter" then
if id:find("claude", 1, true) then
accent, format = Color3.fromRGB(205, 120, 80), "anthropic"
elseif id:find("gpt", 1, true) then
accent = Color3.fromRGB(16, 163, 127)
enRequires API KEYS OpenRouter AgentRouter Opencode Zen [Project is scrapped, meaning this will be a project i never work on again] Inspiration - UAI (DC will be linked)
Never paste your .ROBLOSECURITY cookie anywhere, and use an alt account with scripts.

Universal