The Console
The built-in console is a port of Centurion's terminal UI: same layout, spacing, sizes, ordering and keyboard behaviour. It is delivered with the bundle and mounts itself; there is nothing to require and nothing to place.
The built-in console. Press F2 in game — or click above to drive this one.
Press the activation key (default F2) to toggle it. Argument suggestions come from the argument's type, are filtered as you type, and show the type's own error inline when what you have typed so far cannot be resolved. Clicking away dismisses the console unless hideOnLostFocus is off.
Keyboard
| Key | With suggestions showing | Otherwise |
|---|---|---|
| Up / Down | move through the suggestion list | walk command history |
| Tab | accept the selected row, or complete the top suggestion | none |
| Enter | accept the selected row | run the command |
Selection clamps at both ends rather than falling through into history, so the two modes never blur together. Typing clears the selection, as does a suggestion resolving late and changing the list.
Esc is deliberately not bound. The Roblox client takes it before the game does and opens its own menu, so a console binding on it would fire only sometimes, which is worse than not having one. Dismissal is covered by the activation key and clicking away.
The console slides down into place and back out, settling in around 0.12 s. Every way of dismissing it, the activation key and clicking away, takes the same animated path, and input is dead from the moment a close begins, so a keystroke during one reaches neither the console nor the game behind it. Toggling mid-animation retargets rather than restarting.
History is capped
The console keeps the most recent 500 lines by default and drops the oldest silently. Change it with Client = { HistoryLimit = 1000 }; 0 means uncapped.
A reply per player fills it fast
At 500 CCU a listing command that replies one line per player produces ~500 rows in a single invocation, so it alone fills the default. Summarise, paginate, or cap replies inside such a command.
On touch devices
A touch player has no activation key, so the console would otherwise be unreachable. When the client reports no keyboard, an icon appears in the topbar. Tapping it toggles the console through exactly the same path as the activation key.
The icon is drawn by TopbarPlus. If the game already runs an older copy of TopbarPlus, Operator falls back to its own floating button in the top-right and says so through your logger.
The icon is part of the interface, not a separate delivery: Client.UI = false means no icon, and a player who is allowed nothing never receives one. Detection follows KeyboardEnabled rather than TouchEnabled, so a laptop with a touchscreen keeps the key. It re-evaluates if a keyboard is connected or removed mid-session, and the icon is destroyed the moment one appears.
Client = {
Icon = "rbxassetid://123456789", -- your own image
-- Icon = false, -- no icon; drive the console with Api.toggle()
}Set Client.Icon = false if your game already draws its own topbar button, and open the console with Api.toggle(). See The topbar icon.
Suggestion rows grow to a comfortable tap target on touch and are accepted with a single tap. Desktop metrics are unchanged.
Themes
Client = { Theme = "operator" }Three ship. All are dark, all use the same twelve tokens and the same relative luminance steps, so layout and readability are identical between them:
| Theme | Highlight | Feel |
|---|---|---|
signal (default) | teal #35C9C0 | cool, high-contrast |
operator | amber #F2A93B | warm, neutral greys |
iris | violet #8E7BF0 | muted, low-glare |
The palette is a live source rather than a captured option, so changing the theme recolours the existing instances instead of requiring a remount. No component holds a colour literal; every colour comes from a token, which is what makes a theme a twelve-value table and nothing more.
Your own colours
Client.Theme also takes a table of token overrides:
Client = {
Theme = {
highlight = Color3.fromHex("FF7A59"),
success = Color3.fromHex("6FCF97"),
},
}Overrides merge over the default theme, so a partial table can never leave a colour unset. The other ten tokens come from signal.
Each theme is resolved independently
Overrides merge over the default, never over whatever is currently applied. Two partial themes do not accumulate. The second one starts from signal again, discarding the first. Pass every token you want changed in one table.
The twelve tokens
| Token | Where it lands |
|---|---|
background | the console body, the suggestion list, the log panel and the confirm dialog |
surface | raised areas on top of it: the input field, history rows, log rows, the touch button |
text | primary text: your input, replies, log values |
subtext | secondary text: argument hints, placeholders, timestamps, column headers |
highlight | the accent: the selected suggestion, the active tab and filter, focus edges |
success | a command that succeeded, in history and in the log |
error | a failed command, an invalid argument, the destructive confirm |
severityBan | the Ban Logs severity pill |
severityKick | the Kick severity pill |
severityWipe | the Wipe severity pill |
severityWarn | the Warn severity pill |
severityCustom | any severity you log that is not one of the four above |
A worked theme, changing everything a light scheme would need:
local function hex(value: string): Color3
return Color3.fromHex(value)
end
Operator.Start({
Client = {
Theme = {
background = hex("F4F5F7"),
surface = hex("FFFFFF"),
text = hex("1B1F23"),
subtext = hex("6A737D"),
highlight = hex("0969DA"),
success = hex("1A7F37"),
error = hex("CF222E"),
severityBan = hex("CF222E"),
severityKick = hex("0969DA"),
severityWipe = hex("8250DF"),
severityWarn = hex("BF8700"),
severityCustom = hex("6A737D"),
},
},
})Colours only
Spacing, sizes, corner radii and layout are not configurable, and neither are the two outline constants that separate the console from whatever is behind it. A theme changes what the console looks like, not how it is built. If you need a different shape, use Client.Mount and build the interface yourself.
Both forms are validated at Start, on the server, not on the client. An unknown theme name lists the three valid ones; an unknown token names the offending key and lists all twelve; a value that is not a Color3 names the token and says what it should have been. You never get a console that silently renders in the wrong colours.
Everything client-side is bounded
An admin console runs on a moderator's machine for hours. Anything that accumulates per keystroke or per command is bounded by the package, not by someone noticing:
| What | Bound |
|---|---|
| Displayed history | Client.HistoryLimit, default 500 lines |
| Command input history (Up/Down) | the same limit |
| Suggestion cache entries | 128, evicting expired first then soonest-to-expire |
| In-flight dispatches | cleared on result or timeout |
| Log lines per key | 3 per 60s, then a counted summary |
Confirming destructive commands
A :destructive() command asks the player before running. The server sends a confirmation request and waits (up to 30 seconds) for an answer; the client answers through a handler the UI installs:
Api.setConfirmHandler(function(prompt)
return showDialog(prompt)
end)With no handler installed the answer is no, so destructive commands fail closed.