Extend¶
smelt exposes a Lua API on the global smelt table. With it you can register
slash commands, react to events, add tools the agent can call, build dialogs and
overlays, and change how the transcript looks. Extensions are plain Lua files:
no manifest, no build step.
Plugins are trusted code
Lua config and plugins run in-process with your user's privileges and are
not sandboxed. Review third-party plugins and project .smelt/ config before
you trust them.
Just ask the agent
The built-in customize skill knows the Lua API and your config layout, so
the agent can write, install and reload a plugin for you. Describe what you
want, then treat the result as a starting point.
The bundled plugins in
runtime/lua/smelt/plugins/
are the best examples; every pattern in these guides comes from them.
For small plugins to copy, recorded in action, see the
plugin gallery.
Anatomy of a plugin¶
A plugin is a Lua module. Top-level statements run when it loads, so that is where you register behavior.
-- ~/.config/smelt/plugins/hello.lua
local M = smelt.plugin("hello")
M.state.greetings = M.state.greetings or 0
smelt.cmd.register("hello", function(arg)
M.state.greetings = M.state.greetings + 1
local name = (arg and arg ~= "") and arg or "world"
smelt.notify.info("hello, " .. name)
end, { desc = "greet someone" })
return M
Save it and /hello is available straight away:
smelt.plugin(name) gives the module a stable identity. It exposes M.state,
which survives /reload, and it names the buffers, windows and overlays you
create so they reconnect after a reload. It does not prefix command, keymap or
event names, so pick distinctive ones.
Files in ~/.config/smelt/plugins/ and .smelt/plugins/ load automatically.
Reusable modules under ~/.config/smelt/lua/ load with require:
require("smelt.plugins.which_key") -- bundled opt-in plugin
require("hello") -- ~/.config/smelt/lua/hello.lua
Editor setup¶
smelt mirrors its Lua runtime, including type stubs for every API, to
~/.local/share/smelt/builtins/lua/ (%LOCALAPPDATA%\smelt\data\builtins\lua\
on Windows). Point Lua Language Server at it for completions and diagnostics.
Treat the folder as read-only; upgrades replace it.
{
"runtime.version": "Lua 5.4",
"workspace.library": ["/home/you/.local/share/smelt/builtins/lua"],
"diagnostics.globals": ["smelt"]
}
In a source checkout, use runtime/lua as the library path.
Host and UiHost¶
Every API belongs to one of two tiers, shown on its reference page.
- Host works everywhere, including
smelt --headless:smelt.fs,smelt.http,smelt.process,smelt.events,smelt.tools, and so on. - UiHost needs the terminal UI and raises in headless mode:
smelt.win,smelt.buf,smelt.dialog,smelt.overlay,smelt.prompt, and so on. A few modules, such assmelt.themeandsmelt.notify, mix both: each reference entry states which tier it needs.
Keep core logic on Host APIs and presentation on UiHost APIs, and the same plugin works in the TUI and in CI.
Hot reload¶
Saved Lua files reload automatically; F5 or /reload reloads manually. The
transcript and agent state stay put, and a failing file leaves the previous
config running (see Reloading).
To keep UI state across reloads:
- Start the module with
smelt.plugin(name). - Keep rebuildable state, such as whether a panel is open, in
M.state. It survives/reloadbut not a restart. - Re-open the UI from the module body when that state says it was open. Named buffers, windows and overlays reconnect instead of duplicating.
local M = smelt.plugin("my_plugin")
local function open()
local buf = smelt.buf.new()
local win = smelt.win.new(buf, { surface = "readonly_text" })
smelt.overlay.new({ layout = smelt.ui.layout.leaf(win) })
M.state.is_open = true
end
if M.state.is_open then open() end
return M
Pass an explicit opts.name when a resource must keep its identity across
reordering or when you create several dynamically. Without smelt.plugin,
resources are anonymous and are removed on reload.
Use smelt.lifecycle.on_ready(fn) for code that must run after all plugins have
loaded. It receives ctx.kind ("launch" or "reload") so launch-only work can
skip reloads.
Plugin state¶
smelt.state.get(name) returns a table that survives /reload but not a
restart. Use it for UI state and caches.
smelt.state.persistent(name) is backed by a JSON file in the state directory
(see Storage and environment) and survives restarts.
Top-level assignments save automatically; after changing nested values call
.save().
local s = smelt.state.persistent("recent_files")
s.last_opened = "/path/to/file" -- saved automatically
s.history = s.history or {}
table.insert(s.history, "another")
s.save() -- nested change: save explicitly
String-literal aliases¶
Parameters typed smelt.<namespace>.<Name> accept a closed set of strings, and
your editor autocompletes and checks them. Use the canonical name:
smelt.vim.set_mode("normal") works, "n" does not. Open aliases such as
smelt.signal.Name accept any string
and list well-known names as hints.