Files
community-plugins/llamanager/panel.luau
T
Marc Tristan VictoriaandGitHub f228731141 Add llamanager plugin (#171)
* feat: add Llamanager plugin

* fix image paths in README

* docs: fix validation issues

* fix: handle missing trailing separator in modelfile directory

* fix: escape user input in shell commands

* fix: disable modelfile editor when path is unset or misconfigured

* fix: handle invalid Modelfile directory

* docs: update README

* fix: reload launcher model from config

* docs: update demo assets
2026-08-01 08:58:54 -04:00

342 lines
12 KiB
Luau

local modelfilePath = ""
local pathSet
local stageIcon = "topology-star-3"
-- models and runtime view
function onRefreshButtonClick()
noctalia.state.set("llamanager.nextCommand", "refresh")
end
function onLaunchButtonClick()
noctalia.state.set("llamanager.nextCommand", "launch")
end
function onRuntimeButtonClick()
noctalia.state.set("llamanager.nextCommand", "runtime")
end
function onBackButtonClick()
noctalia.state.set("llamanager.nextCommand", "refresh")
noctalia.state.set("llamanager.view", "modelsView")
end
function onEditorButtonClick()
if not pathSet then
noctalia.notify("Llamanager", "Please configure a valid Modelfile directory first.")
return
end
noctalia.state.set("llamanager.nextCommand", "editor")
end
function onDownloadButtonClick()
noctalia.state.set("llamanager.nextCommand", "download")
end
function changeSelectedIndexModel(index)
noctalia.state.set("llamanager.selectedModel", index)
end
function changeDownloadModelField(downloadRequest)
noctalia.state.set("llamanager.downloadModelField", {modelName=downloadRequest, data=nil, result=nil})
end
-- editor view
function onLoadOllamaButtonClick()
noctalia.state.set("llamanager.nextCommand", "loadToOllama")
end
function onCreateButtonClick()
noctalia.state.set("llamanager.nextCommand", "create")
end
function onEditButtonClick()
noctalia.state.set("llamanager.nextCommand", "edit")
end
function onDeleteButtonClick()
noctalia.state.set("llamanager.nextCommand", "delete")
end
function changeModelfileName(fileName)
noctalia.state.set("llamanager.modelfileNameField", fileName)
end
function changeModelfileContent(content)
noctalia.state.set("llamanager.modelfileContent", content)
local path = modelfilePath .. noctalia.state.get("llamanager.modelfileNameField") .. ".modelfile"
noctalia.writeFile(path, content)
end
local function renderModelsView(models)
local modelRows = {}
local function getModelOptions()
local options = {"Default (Ollama)"}
for _, model in ipairs(models) do
table.insert(options, model.name)
end
return options
end
local function populateModelRows(models, modelRows)
if #models ~= 0 then
for _, model in ipairs(models) do
table.insert(modelRows,
ui.row({ justify = "space_between" }, {
ui.label({
text = "└── " .. model.name,
}),
})
)
end
else
table.insert(modelRows,
ui.label({text = "No models installed... :("})
)
end
end
populateModelRows(models, modelRows)
panel.render(
ui.column({ flexGrow = 1 }, {
-- header
ui.row({ justify = "space_between" }, {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = stageIcon, color = "primary"}),
ui.label({ text = "Llamanager", fontSize = 18, fontWeight = "bold", color = "on_surface",}),
}),
ui.row({ gap = 8, align = "center" }, {
ui.button({ glyph = "activity", onClick = "onRuntimeButtonClick", tooltip = "Runtime Dashboard",}),
ui.button({ glyph = "refresh", onClick = "onRefreshButtonClick" }),
}),
}),
ui.scroll({ flexGrow = 1 }, {
ui.label({ text = "Model List", fontWeight = "bold", color = "primary" }),
ui.spacer({ height = 8 }),
ui.column({ gap = 6 }, modelRows),
ui.spacer({ height = 8 }),
ui.label({ text = "Modelfile Editor", fontWeight = "bold", color = "primary",}),
ui.spacer({ height = 8 }),
ui.button({ text = "Open Editor", onClick = "onEditorButtonClick" }),
ui.spacer({ height = 8 }),
ui.label({ text = "Model Downloader ", color = "primary", fontWeight = "bold"}),
ui.spacer({ height = 8 }),
ui.row({ gap=8 }, {
ui.input({ flexGrow = 1, placeholder="llama3.1:8b, deepseek-r1:8b, ...", onChange="changeDownloadModelField"}),
ui.button({ glyph="download", onClick = "onDownloadButtonClick" }),
}),
}),
-- footer
ui.column({ gap = 8 }, {
ui.row({ gap = 8, justify = "end"}, {
ui.select({ placeholder = "Model", flexGrow=1, options = getModelOptions(), onChange = "changeSelectedIndexModel" }),
ui.button({ glyph = "rocket", onClick = "onLaunchButtonClick" }),
}),
}),
})
)
end
local function renderRuntimeView(ollama)
local ollamaRows = {}
local runtimeRows = {}
local function populateRuntimeRows(runtime, runtimeRows)
for i, model in ipairs(runtime) do
if next(model) ~= nil then
local date, time = "-", "-"
if model.expires then
date, time = model.expires:match("(%d%d%d%d%-%d%d%-%d%d)T(%d%d:%d%d:%d%d)")
end
table.insert(runtimeRows,
ui.column({gap = 6 }, {
ui.label({ text = "[" .. i .. "] " .. model.name , color = "primary" }),
ui.label({ text = "Parameters\t" .. (model.parameterSize or "-") }),
ui.label({ text = "Quantization\t" .. (model.quantization or "-") }),
ui.label({ text = "Context\t\t" .. tostring(model.context or "-") }),
ui.label({ text = "Expires\t\t" .. date .. " | ".. time}),
})
)
end
end
if next(runtimeRows) == nil then
table.insert(runtimeRows,
ui.column({}, {
ui.label({text = "No models are currently running :)"})
}))
end
end
local function populateOllamaRows(server, ollamaRows)
table.insert(ollamaRows,
ui.column({ gap = 6 }, {
ui.label({ text = "Installed:\t" .. tostring(server.installed)}),
ui.label({ text = "Version:\t" .. tostring(server.version)}),
ui.label({ text = "Executable:\t" .. server.executable}),
ui.label({ text = "Endpoint:\t" .. server.endpoint}),
ui.label({ text = "API Connected:\t" .. tostring(server.api_connected)}),
})
)
end
populateRuntimeRows(ollama.runtime, runtimeRows)
populateOllamaRows(ollama.server, ollamaRows)
-- runtime dashboard
panel.render(
ui.column({flexGrow=1, justify="space_between"}, {
-- header
ui.row({ justify = "space_between" }, {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = stageIcon, color = "primary"}),
ui.label({ text = "Llamanager", fontSize = 18, fontWeight = "bold", color = "on_surface" }),
}),
ui.row({ gap = 8, align = "center" }, {
ui.button({ glyph = "arrow-left", onClick = "onBackButtonClick" }),
ui.button({ glyph = "refresh", onClick = "onRefreshButtonClick" }),
}),
}),
ui.scroll({ flexGrow = 1}, {
ui.label({ text = "Runtime Dashboard", fontWeight = "bold", color = "primary" }),
ui.spacer({ height = 8 }),
ui.label({ text = "Ollama", color = "primary" }),
ui.column({ gap = 6 }, ollamaRows),
ui.separator({spacing = 8,}),
ui.label({ text = "Active Models", color = "primary" }),
ui.label({text = "Installed: " .. #ollama.models,}),
ui.spacer({height = 8,}),
ui.column({ gap = 6 }, runtimeRows),
}),
})
)
end
local function renderEditorView()
local modelDirRows = {}
local function populateModelDirRows(modelDirRows)
local dirs = noctalia.state.get("llamanager.modelfilePaths")
for i, dir in ipairs(dirs) do
table.insert(modelDirRows,
ui.row({ justify = "space_between" }, {
ui.label({
text = "└── " .. dir,
}),
})
)
end
end
populateModelDirRows(modelDirRows)
panel.render(
ui.column({flexGrow=1, justify="space_between"}, {
-- header
ui.row({ justify = "space_between" }, {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = stageIcon, color = "primary"}),
ui.label({ text = "Llamanager", fontSize = 18, fontWeight = "bold", color = "on_surface", }),
}),
ui.row({ gap = 8, align = "center" }, {
ui.button({ glyph = "arrow-left", onClick = "onBackButtonClick" }),
ui.button({ glyph = "refresh", onClick = "onRefreshButtonClick" }),
}),
}),
-- body
ui.scroll({ flexGrow = 1 }, {
ui.label({ text = "Modelfiles", fontWeight = "bold", color = "primary",}),
ui.label({text = "Modelfile Path: " .. modelfilePath,}),
ui.spacer({height = 8,}),
ui.column({}, modelDirRows)
}),
-- foot
ui.row({ gap = 8 }, {
ui.button({ glyph = "file-code", onClick = "onLoadOllamaButtonClick", tooltip="Load to Ollama" }),
ui.input({ flexGrow = 1, placeholder = "Modelfile Name", onChange = "changeModelfileName" }),
ui.button({ glyph = "plus", onClick = "onCreateButtonClick", tooltip="Create" }),
ui.button({ glyph = "edit", onClick = "onEditButtonClick", tooltip="Edit" }),
ui.button({ glyph = "trash", onClick = "onDeleteButtonClick", tooltip="Delete"}),
}),
})
)
end
local function renderModelfileEditorView()
local path = modelfilePath .. noctalia.state.get("llamanager.modelfileNameField") .. ".modelfile"
content = noctalia.readFile(path)
panel.render(
ui.column({}, {
-- header
ui.row({ justify = "space_between" }, {
ui.row({ gap = 8, align = "center" }, {
ui.glyph({ name = stageIcon, color = "primary"}),
ui.label({ text = "Llamanager", fontSize = 18, fontWeight = "bold", color = "on_surface" }),
}),
ui.row({ gap = 8, align = "center" }, {
ui.button({ glyph = "arrow-left", onClick = "onBackButtonClick" }),
ui.button({ glyph = "refresh", onClick = "onRefreshButtonClick" }),
}),
}),
ui.label({ text = "Editing " .. path }),
ui.spacer({ height = 2, flexGrow = 0}),
-- body
ui.input({ value = content, flexGrow = 1, placeholder = "Modelfile", onChange = "changeModelfileContent", multiline = true}),
})
)
end
-- main
function onOpen(_context)
modelfilePath = noctalia.expandPath(noctalia.getConfig("modelfile_path"))
if not modelfilePath or modelfilePath == "" or not noctalia.fileExists(modelfilePath) then
pathSet = false
else
if modelfilePath:sub(-1) ~= "/" then
modelfilePath = modelfilePath .. "/"
end
pathSet = true
end
-- trigger initial load
noctalia.state.set("llamanager.view", "modelsView")
noctalia.state.set("llamanager.nextCommand", "refresh")
end
local function render()
local ollama = noctalia.state.get("llamanager.ollama") or {}
local view = noctalia.state.get("llamanager.view")
if view == "modelsView" then
renderModelsView(ollama.models or {})
elseif view == "runtimeView" then
renderRuntimeView(ollama or {})
elseif view == "editorView" and pathSet then
renderEditorView()
elseif view == "modelfileEditorView" then
renderModelfileEditorView()
end
end
-- UI updates
noctalia.state.watch("llamanager.view", render)
noctalia.state.watch("llamanager.ollama", render)