Plugin gallery¶
Small plugins to copy into ~/.config/smelt/plugins/. Each one is recorded
running in smelt, with a live model doing the work.
Crate lookup¶
A new tool for the agent: the latest version, downloads and description of a crate, straight from crates.io.
-- ~/.config/smelt/plugins/crates.lua
smelt.tools.register({
name = "crate_info",
description = "Latest version and downloads of a crate on crates.io.",
parameters = {
type = "object",
properties = { name = { type = "string" } },
required = { "name" },
},
execute = function(args)
local url = "https://crates.io/api/v1/crates/" .. args.name
local headers = { ["user-agent"] = "smelt" }
local res, err = smelt.http.get(url, { headers = headers })
if not res or res.status ~= 200 then
return { content = err or "crate not found", is_error = true }
end
local c = smelt.json.decode(res.body).crate
return ("%s %s, %d downloads: %s"):format(
c.name, c.max_stable_version, c.downloads, c.description)
end,
})
Statusline¶
A lualine-style rebuild of the whole statusline: the mode, the working directory, the branch and counts of added, modified and removed files, with Nerd Font icons.
-- ~/.config/smelt/plugins/lualine.lua
local A, B, C = { 232, 100, 26 }, { 58, 47, 39 }, { 30, 26, 21 }
local FG, DARK = { 232, 223, 214 }, { 23, 20, 15 }
local DIFF = {
{ "added", " ", { 143, 191, 106 } },
{ "modified", " ", { 230, 180, 80 } },
{ "removed", " ", { 239, 107, 91 } },
}
local branch, counts = "", {}
-- Read the branch and count changed files after every tool the agent runs.
local function refresh()
smelt.spawn(function()
local snap = smelt.git.diff()
branch, counts = snap and snap.branch or "", { added = 0, modified = 0, removed = 0 }
for _, f in ipairs(snap and snap.diff:files() or {}) do
local kind = (f.status == "?" or f.status == "A") and "added"
or f.status == "D" and "removed" or "modified"
counts[kind] = counts[kind] + 1
end
smelt.statusline.invalidate()
end)
end
smelt.events.on("tool_end", refresh)
refresh()
local function text(items, s, bg, fg, bold)
table.insert(items, { text = s, style = { bg = bg, fg = fg, bold = bold } })
end
smelt.statusline.remove("core")
smelt.statusline.add("line", function()
local items = {}
local cwd = smelt.path.basename(smelt.os.cwd())
text(items, " " .. (smelt.signal.get("agent_mode") or "") .. " ", A, DARK, true)
text(items, "", B, A)
text(items, " " .. cwd .. " ", B, FG)
text(items, "", C, B)
text(items, " " .. branch .. " ", C, FG)
for _, d in ipairs(DIFF) do
local n = counts[d[1]] or 0
if n > 0 then text(items, d[2] .. n .. " ", C, d[3]) end
end
return items
end)
Spinner¶
The working indicator in the rule above the prompt is a source named
indicator. The built-in one is a coal that heats with tool calls, beside a gray
word that a hammer sweep runs along. Replacing the source draws anything you
like. Here status words decode in from noise, hold with the odd flicker and a
pink and cyan tear, then corrupt away and give way to the next word. When the
turn is done, "done" decodes in one last time and holds, with no dot.
Each frame is a pure function of smelt.time.now_ms(), so it repaints the same
however often the bar redraws. When the turn is interrupted or paused, the
built-in indicator takes over again through builtin("indicator").
glitch.lua
-- ~/.config/smelt/plugins/glitch.lua
local fallback = smelt.prompt_bar.top.builtin("indicator")
-- A glitching status word: each word decodes in from noise, holds with the odd
-- flicker, then corrupts away into the next one. It stays white, with a pink and
-- cyan fringe on the noise. When the turn is done the word "done" decodes in one
-- last time and holds still, with no dot.
local DONE = "done"
local WORDS = { "parsing", "linking", "patching", "resolving", "compiling" }
local WIDTH = 9
local CYCLE_MS = 2600
local DECODE_MS = 520
local CORRUPT_MS = 440
local FRAME_MS = 70
local NOISE = { "░", "▒", "▓", "█", "#", "%", "&", "@", "$", "/", "<", ">", "=", "+", "*", "?" }
local function palette()
if smelt.theme.is_light() then
return {
text = { 40, 40, 45 },
dark = 60,
bright = 150,
magenta = { 200, 30, 120 },
cyan = { 0, 140, 170 },
}
end
return {
text = { 240, 240, 245 },
dark = 170,
bright = 255,
magenta = { 240, 70, 165 },
cyan = { 40, 215, 235 },
}
end
-- Repeatable noise in 0..1, so a frame looks the same however often it repaints.
local function noise(a, b)
local x = math.sin(a * 12.9898 + b * 78.233) * 43758.5453
return x - math.floor(x)
end
local function working()
local state = smelt.signal.get("work_state")
return state == "working" or state == "retrying" or state == "busy"
end
-- Noise is mostly a random gray; now and then it fringes pink or cyan.
local function static(colors, a, b)
local pick = noise(a * 3, b + 13)
if pick < 0.12 then return colors.magenta end
if pick < 0.24 then return colors.cyan end
local level = math.floor(colors.dark + (colors.bright - colors.dark) * noise(a, b + 17))
return { level, level, level }
end
-- One frame of `word`: each letter is noise until it decodes in. While `live`,
-- the word also flickers, tears, and corrupts away at the end of the cycle.
local function render(colors, word, width, phase, round, live)
local frame = math.floor(smelt.time.now_ms() / FRAME_MS)
-- Now and then the whole word tears into a magenta and cyan split.
local torn = live and phase > DECODE_MS and noise(frame, round + 3) > 0.96
-- A single letter flickers while the word holds.
local flicker = live and phase > DECODE_MS and noise(frame, round + 7) > 0.9
and math.floor(noise(frame, round + 11) * #word) + 1
local runs = {}
for i = 1, width do
local letter = word:sub(i, i)
local color = colors.text
local garbled = false
if phase < DECODE_MS * 0.9 * (i / #word) then
garbled = true
elseif live and phase > CYCLE_MS - CORRUPT_MS then
garbled = noise(i, round) < (phase - (CYCLE_MS - CORRUPT_MS)) / CORRUPT_MS
elseif flicker == i then
garbled = true
end
if letter == "" then
letter = " "
elseif garbled then
letter = NOISE[math.floor(noise(i + frame, round) * #NOISE) + 1]
color = static(colors, i, frame)
elseif torn then
color = noise(i, round) < 0.5 and colors.magenta or colors.cyan
end
runs[#runs + 1] = { text = letter, style = { fg = color, bold = true } }
end
return { { runs = runs, priority = 1 } }
end
-- Nothing else repaints a finished turn, so a timer drives the decode.
local done_at, decoding
smelt.signal.subscribe("work_state", function(state)
if state ~= "done" then
done_at = nil
return
end
done_at = smelt.time.now_ms()
if decoding then decoding:remove() end
decoding = smelt.timer.every(FRAME_MS, function() smelt.prompt_bar.top.invalidate() end)
smelt.timer.set(DECODE_MS + FRAME_MS, function()
decoding:remove()
decoding = nil
end)
end)
smelt.prompt_bar.top.add("indicator", function()
if smelt.signal.get("work_state") == "done" and not smelt.signal.get("auto_continue_status") then
local phase = done_at and smelt.time.now_ms() - done_at or DECODE_MS
return render(palette(), DONE, #DONE, phase, 0, false)
end
if not working() then return fallback() end
local now = smelt.time.now_ms()
local round = math.floor(now / CYCLE_MS)
return render(palette(), WORDS[round % #WORDS + 1], WIDTH, now % CYCLE_MS, round, true)
end)
Changes sidebar¶
A file tree of the working copy beside the usual layout, with each file's
added and removed lines, refreshed after every tool the agent runs. It wraps
the default layout from require("smelt.layout").compose instead of rebuilding
it.
-- ~/.config/smelt/plugins/changes.lua
local layout = smelt.ui.layout
local default = require("smelt.layout").compose
local files = smelt.win.new(smelt.buf.new(), { surface = "list_inert", wrap = false, pad_left = 1 })
-- A solid panel in the scroll pill's color.
files:row_highlights({ { start = 0, ["end"] = 1000, hl_group = "SmeltScrollPillBg" } })
-- Re-read the working tree after every tool the agent runs.
local function refresh()
smelt.spawn(function()
local snap = smelt.git.diff()
files:document(snap and snap.diff:tree():document())
end)
end
smelt.events.on("tool_end", refresh)
refresh()
layout.set(function(state)
return layout.hbox({
{ default(state), width = "fill" },
{ layout.leaf(files), width = 34 },
})
end)
Snake¶
Built in: /snake (or F11) opens a snake you steer with hjkl or the arrows
while the agent works. The title keeps the score and the session's best.
snake.lua
-- /snake (or F11) - snake in a floating window. hjkl or the arrows steer,
-- space starts over, q, Esc or Ctrl-C closes.
-- The title shows the score and the best one this session.
--
-- Two board rows share one terminal row: `▀` paints the top half in its
-- foreground and the bottom half in its background.
local W, H = 32, 32
local TICK_MS = 100
local FOOD = { rgb = { 240, 80, 60 } }
local STEP = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
local OPPOSITE = { up = "down", down = "up", left = "right", right = "left" }
local session = smelt.state.get("smelt.snake")
local game, ui, show
local function key(x, y) return y * W + x end
-- A random free cell, or nil once the snake fills the board.
local function place_food(snake)
local taken, free = {}, {}
for _, p in ipairs(snake) do taken[key(p[1], p[2])] = true end
for i = 0, W * H - 1 do
if not taken[i] then free[#free + 1] = i end
end
if #free == 0 then return nil end
local i = free[math.random(#free)]
return { i % W, i // W }
end
local function new_game()
local y = H // 2
local snake = { { 7, y }, { 6, y }, { 5, y } } -- head first
game = { snake = snake, dir = "right", next_dir = "right", food = place_food(snake), score = 0, dead = false }
end
local function tick()
if game.dead then return end
game.dir = game.next_dir
local head, d = game.snake[1], STEP[game.dir]
local x, y = head[1] + d[1], head[2] + d[2]
local hit = x < 0 or y < 0 or x >= W or y >= H
-- The tail moves out of the way this tick, so it is not an obstacle.
for i = 1, #game.snake - 1 do
local p = game.snake[i]
if p[1] == x and p[2] == y then hit = true end
end
if hit then
game.dead = true
return show()
end
table.insert(game.snake, 1, { x, y })
if game.food and x == game.food[1] and y == game.food[2] then
game.score = game.score + 1
if game.score > (session.best or 0) then session.best = game.score end
game.food = place_food(game.snake)
show()
else
table.remove(game.snake)
end
end
-- The head is bright and the body fades toward the tail.
local function shade(i)
local f = 1 - (i - 1) / math.max(#game.snake, 8) * 0.7
return { rgb = { math.floor(120 * f), math.floor(220 * f), math.floor(90 * f) } }
end
local function paint(slice)
if not game then return end
local bg = smelt.theme.get("SmeltScrollPillBg").bg
local cells = {}
for i, p in ipairs(game.snake) do cells[key(p[1], p[2])] = shade(i) end
if game.food then cells[key(game.food[1], game.food[2])] = FOOD end
local cols, rows = math.min(W, slice:width()), math.min(H // 2, slice:height())
slice:fill_rect(0, 0, cols, rows, " ", { bg = bg })
for row = 0, rows - 1 do
for x = 0, cols - 1 do
local top, bottom = cells[key(x, row * 2)], cells[key(x, row * 2 + 1)]
if top or bottom then slice:set(row, x, "▀", { fg = top or bg, bg = bottom or bg }) end
end
end
end
local function turn(dir)
return function()
if not game.dead and game.dir ~= OPPOSITE[dir] then game.next_dir = dir end
end
end
local function restart()
new_game()
show()
end
local function close()
if not ui then return end
ui.timer:remove()
ui.paint:remove()
ui.win:close()
ui, game = nil, nil
end
-- Re-opening the named overlay refreshes its title with the score.
show = function()
local title = (" snake %d · best %d "):format(game.score, session.best or 0)
if game.dead then title = title .. "· game over " end
local layout = smelt.ui.layout
smelt.overlay.new({
name = "smelt.snake",
title = title,
border = { all = "Comment" },
anchor = "screen_at", corner = "ne", row = 1, col = 0,
width = W + 2, height = H // 2 + 2,
-- The empty window holds focus so the keymaps fire; the board is a paint.
layout = layout.vbox({
{ layout.leaf(ui.win), height = 0 },
{ layout.leaf(ui.paint, { measure = { W, H // 2 } }), height = "fit" },
}),
modal = false,
draggable = true,
resizable = false,
keymaps = {
{ key = "h", on_press = turn("left") },
{ key = "j", on_press = turn("down") },
{ key = "k", on_press = turn("up") },
{ key = "l", on_press = turn("right") },
{ key = "<Left>", on_press = turn("left") },
{ key = "<Down>", on_press = turn("down") },
{ key = "<Up>", on_press = turn("up") },
{ key = "<Right>", on_press = turn("right") },
{ key = "<Space>", on_press = restart },
{ key = "q", on_press = close },
{ key = "<Esc>", on_press = close },
{ key = "<C-c>", on_press = close },
},
})
end
local function open()
math.randomseed(os.time())
new_game()
ui = { win = smelt.win.new(smelt.buf.new(), { surface = "readonly_text", hide_cursor = true }) }
ui.paint = smelt.paint.register(paint, { name = "smelt.snake.paint" })
show()
ui.win:focus()
ui.timer = smelt.timer.every(TICK_MS, tick)
end
local function toggle()
if ui then close() else open() end
end
smelt.cmd.register("snake", toggle, { desc = "play snake" })
smelt.keymap.set("", "<F11>", toggle)