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
This commit is contained in:
Marc Tristan Victoria
2026-08-01 08:58:54 -04:00
committed by GitHub
parent f369cfc972
commit f228731141
11 changed files with 952 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 marccvictoria
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+93
View File
@@ -0,0 +1,93 @@
# Llamanager
> **A Noctalia v5 plugin for managing local Ollama models, Modelfiles, and runtime state.**
Llamanager provides a graphical frontend for [Ollama](https://ollama.com/) inside Noctalia. It wraps the Ollama CLI and HTTP API into a single panel, and allows model management, runtime inspection, model downloads, Modelfile editing, and launcher integration without requiring direct terminal interaction.
## Demo
![Launcher](assets/launcher.gif)
![Panel](assets/panel.gif)
## Plugin
| Field | Value |
| --------------- | ---------------------------------------------------------------------------------------- |
| ID | `marccvictoria/llamanager` |
| Entries | bar widget: `widget`; service: `registry`; launcher_provider: `launcher`; panel: `panel` |
| Launcher Prefix | `/ll` |
## Usage
Before using Llamanager, complete the following setup steps:
1. Install Ollama and ensure it is available on your `PATH`. See [Requirements](#requirements).
2. Open the plugin settings and configure:
- the Modelfile directory
- the preferred AI model used by the launcher
3. Add the Llamanager widget to your Noctalia bar.
## Requirements
- **Noctalia v5**
- `ollama` available on your `PATH`: required for model management, downloads, launching models, and Modelfile operations.
Verify the installation:
```bash
ollama --version
```
## Settings
| Setting | Type | Description |
| ---------------- | -------- | ------------------------------------- |
| `modelfile_path` | `folder` | Directory containing user Modelfiles. |
| `launcher` | `string` | Model used by the launcher. |
## Implementation
### Panel
`panel.luau` renders whichever view is currently active. User interactions dispatch commands through `llamanager.nextCommand`; the service performs the requested operation, updates state, and the panel re-renders.
`registry.luau` is the headless service responsible for interacting with Ollama. Filesystem operation, CLI invocation, and HTTP request originates here. It discovers installed models through `ollama list`, queries runtime information from the Ollama HTTP API, manages Modelfiles on disk, downloads models through `/api/pull`, and executes `ollama create` and `ollama rm` when building or deleting models.
**Model List**. The list displays every model currently installed in Ollama. To launch a model, select it from the model selector and click the launch button. This executes `ollama run <model>` and opens an interactive session in a new terminal window.
**Model Downloader**. Downloads models directly through Ollama's HTTP API. Downloads execute using the streaming `/api/pull` endpoint and automatically refresh the model library after completion.
**Modelfile Editor**. Modelfiles are ordinary text files stored inside the configured Modelfile directory, with the suffix `.modelfile`.
- **Load into Ollama**. Builds the modelfile into an ollama model by executing ollama create.
- **Delete model/modelfile**. Deleting a modelfile also deletes the model in ollama using `ollama rm`. In case the modelfile has not been loaded yet to ollama, it will only delete the modelfile. When deleting a model, it is recommended to include their tag, e.g. `qwen3:latest`.
- **Create Modelfile**. Creating a Modelfile requires the Name field to be populated. This only creates the Modelfile on the directory and does not register it with Ollama. To make the model available in Ollama, use _Load into Ollama_ after creating or editing the Modelfile.
- **Edit Modelfile**. Editing a Modelfile also requires the Name field to be populated. Saving changes only updates the Modelfile on the directory; it does not update the existing Ollama model. After modifying the Modelfile, use _Load into Ollama_ to rebuild and apply the changes.
**Runtime Dashboard**
Queries `http://localhost:11434/api/ps` and displays:
- Running models
- Parameter size
- Quantization level
- Context length
- Expiration time
- Ollama version
- API connectivity
### Launcher
The launcher entry `llamanger.luau` is independent from the panel. Queries submitted through `/ll` execute the configured model directly with `ollama run`.
The launcher provides quick model execution through `/ll <question> //`. Append `//` to send, the model response can be seen through notification and can be copied in your clipboard by pressing Enter key.
## IPC
```
noctalia msg panel-toggle marccvictoria/llamanager:panel
```
## License
[MIT](LICENSE)
Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

+79
View File
@@ -0,0 +1,79 @@
function onQuery(text)
if text == "" then
launcher.setResults(text, {
{
id = "hint",
title = "Ask Ollama anything",
subtitle = "Finish with // to send",
glyph = "brain",
}
})
return
end
if not text:match("%s//%s*$") then
launcher.setResults(text, {
{
id = "waiting",
title = text,
subtitle = "Append // to send",
glyph = "keyboard",
}
})
return
end
local prompt = text:gsub("%s//%s*$", "")
launcher.setResults(text, {
{
id = "loading",
title = "Thinking...",
subtitle = prompt,
glyph = "loader",
}
})
local request = {
url = "http://localhost:11434/api/generate",
method = "POST",
headers = {
"Content-Type: application/json",
},
body = noctalia.json.encode({
model = noctalia.getConfig("launcher"),
prompt = prompt,
stream = false,
think = false,
}),
}
noctalia.http(request, function(response)
if not (response.ok and response.status == 200) then
launcher.setResults(text, {
{
id = "error",
title = "Ollama request failed",
subtitle = response.body or ("HTTP " .. tostring(response.status)),
glyph = "alert-circle",
}
})
return
end
local data = noctalia.json.decode(response.body)
noctalia.notify("LLamanager", data.response)
launcher.setResults(text, {
{
id = data.response,
title = data.response,
subtitle = "Press Enter to copy",
glyph = "brain",
}
})
end)
end
function onActivate(id)
noctalia.copyToClipboard(id, "text/plain")
end
+342
View File
@@ -0,0 +1,342 @@
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)
+47
View File
@@ -0,0 +1,47 @@
id = "marccvictoria/llamanager"
name = "Llamanager"
version = "1.0.0"
plugin_api = 4
author = "marccvictoria"
license = "MIT"
deprecated = false
icon = "topology-star-3"
description = "Launcher and Manager for Ollama."
tags = ["ai", "productivity", "bar", "launcher", "panel"]
dependencies = ["ollama"]
[[setting]]
key = "modelfile_path"
type = "folder"
default = ""
label_key = "settings.modelfile_path.label"
description_key = "settings.modelfile_path.description"
[[setting]]
key = "launcher"
type = "string"
default = ""
label_key = "settings.launcher.label"
description_key = "settings.launcher.description"
[[widget]]
id = "widget"
entry = "widget.luau"
[[panel]]
id = "panel"
entry = "panel.luau"
open_near_click = true
placement = "floating"
width = 400
height = 350
[[service]]
id = "registry"
entry = "registry.luau"
[[launcher_provider]]
id = "launcher"
entry = "llamanager.luau"
prefix = "ll"
glyph = "robot"
+352
View File
@@ -0,0 +1,352 @@
local modelfilePath = noctalia.expandPath(noctalia.getConfig("modelfile_path"))
if modelfilePath:sub(-1) ~= "/" then
modelfilePath = modelfilePath .. "/"
end
local function joinModelNameToPath(modelName)
return modelfilePath .. modelName .. ".modelfile"
end
local function isNotEmpty(value)
if not value or value:match("^%s*$") then
noctalia.notify("LLamanager", "Field cannot be empty.")
return false
end
return true
end
local function shellEscape(str)
return "'" .. tostring(str):gsub("'", "'\\''") .. "'"
end
-- return ollama table
local function loadOllama()
local isInstalled = function() return noctalia.commandExists("ollama") end
local ollama = {
server = {
executable = "ollama",
installed = isInstalled(),
endpoint = "localhost:11434",
api_connected = false,
version = nil,
},
models = {}, -- models installed
runtime = {}, -- runtime models and other info
}
return ollama
end
-- models dashboard
local function getModels(onDone)
local models = {}
noctalia.runAsync("ollama list | awk 'NR>1 {print $1}'", function(result)
if result.exitCode ~= 0 then
onDone({})
return
end
for line in string.gmatch(result.stdout, "[^\r\n]+") do
local model_name = line:gsub("%s+$", "")
if model_name ~= "" then
table.insert(models,
{
name = model_name,
executable = "ollama run " .. model_name .. " --think=false",
})
end
end
onDone(models)
end)
end
-- runtime dashboard
local function getOllamaVersion(onVersion)
noctalia.runAsync("ollama --version | awk '{print $4}'", function(result)
if result.exitCode ~= 0 then
noctalia.notify("Failed to get Ollama version")
return
end
local version = result.stdout
onVersion(version)
end)
end
local function launch(ollama)
local selectedModel = tonumber(noctalia.state.get("llamanager.selectedModel"))
local models = ollama.models
if selectedModel ~= 0 then
local model = models[selectedModel]
if not model then
noctalia.notify("Selected model not found")
return
end
noctalia.notify(model.name)
noctalia.runInTerminal(model.executable)
else
noctalia.notify("Ollama")
noctalia.runInTerminal(ollama.server.executable)
end
end
local function getRuntimeInfo(onLoad)
local request = {
url = "http://localhost:11434/api/ps",
method = "GET",
headers = { "Accept: application/json" },
follow_redirects = false,
}
noctalia.http(request, function(response)
if not (response.ok and response.status == 200) then
noctalia.notify("Request failed")
return
end
local api_status = true
-- parse
local data = noctalia.json.decode(response.body)
if not data.models or #data.models == 0 then
onLoad({}, api_status)
return
end
-- update models
local info = {}
for _, model in ipairs(data.models) do
table.insert(info, {
name = model.name,
parameterSize = model.details.parameter_size,
quantization = model.details.quantization_level,
context = model.context_length,
expires = model.expires_at,
})
end
onLoad(info, api_status)
end)
end
local function downloadModel(modelName, onProgress, onFinish)
local request = {
url = "http://localhost:11434/api/pull",
method = "POST",
headers = {
"Content-Type: application/json",
"Accept: application/json",
},
body = noctalia.json.encode({
model = modelName,
stream = true,
}),
}
noctalia.httpStream( request,
-- progress
function(line)
local ok, data = pcall(noctalia.json.decode, line)
if not ok then
return
end
if onProgress then
onProgress(data)
end
end,
-- finished
function(result)
if onFinish then
onFinish(result)
end
end
)
end
local function edit()
local listDir, err = noctalia.listDir(modelfilePath)
if not listDir then
noctalia.notify("Llamanager", err)
return
end
modelfileDirs = {}
for _, dir in ipairs(listDir) do
table.insert(modelfileDirs, dir)
end
noctalia.state.set("llamanager.modelfilePaths", modelfileDirs)
end
local function loadToOllama(modelName, modelfilePath)
local cmd = string.format(
'ollama create %s -f %s',
shellEscape(modelName),
shellEscape(modelfilePath)
)
noctalia.runAsync(cmd, function(result)
if result.exitCode == 0 then
noctalia.notify("Model created!")
else
local err = result.stderr ~= "" and result.stderr or result.stdout
local message = err:match("Error:.-[\r\n]") or err:match("Error:.*") or err
message = message:gsub("[\r\n]", "")
noctalia.notify("Llamanager", message)
end
end)
end
local function deleteModelOllama(modelName)
local cmd = string.format('ollama rm %s', shellEscape(modelName))
noctalia.runAsync(cmd, function(result)
if result.exitCode == 0 then
noctalia.notify("Ollama: Removed ".. modelName)
-- refresh model list
noctalia.state.set("llamanager.nextCommand", "refresh")
else
noctalia.notify("LLamanager", "Modelfile successfully deleted, but no matching ollama model was found.")
noctalia.state.set("llamanager.nextCommand", "refresh")
end
end)
end
noctalia.state.watch("llamanager.nextCommand", function(command)
if command == "refresh" then -- load ollama and its models
local ollama = loadOllama()
getModels(function(models)
ollama.models = models
noctalia.state.set("llamanager.ollama", ollama)
noctalia.state.set("llamanager.view", "modelsView")
end)
-- reset field value
noctalia.state.set("llamanager.modelfileNameField", "")
elseif command == "launch" then
local ollama = noctalia.state.get("llamanager.ollama")
launch(ollama)
elseif command == "runtime" then
local ollama = loadOllama()
getModels(function(models)
ollama.models = models
getRuntimeInfo(function(runtime, status)
ollama.runtime = runtime
ollama.server.api_connected = status
getOllamaVersion(function(version)
ollama.server.version = version
noctalia.state.set(
"llamanager.ollama",
ollama
)
noctalia.state.set(
"llamanager.view",
"runtimeView"
)
end)
end)
end)
elseif command == "download" then
local downloadData = noctalia.state.get("llamanager.downloadModelField")
noctalia.notify("Download Started...")
local lastNotified = -30
local hadError = false
downloadModel(
downloadData.modelName,
function(progress)
if progress.error then
hadError = true
noctalia.notify(progress.error)
return
end
if progress.completed and progress.total then
local percent = math.floor(progress.completed / progress.total * 100)
local milestone = math.floor(percent / 30) * 30
if milestone >= 30 and milestone > lastNotified then
lastNotified = milestone
noctalia.notify("Download Progress: " .. tostring(milestone) .. "%")
end
end
end,
function(result)
if hadError then
return
end
if result.ok then
noctalia.notify("Download complete!")
noctalia.state.set("llamanager.nextCommand", "refresh")
else
noctalia.notify(
"Download failed (" ..
tostring(result.status) ..
")"
)
end
end
)
-- editor
elseif command == "editor" then
edit()
noctalia.state.set("llamanager.view", "editorView")
elseif command == "create" then
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local path = joinModelNameToPath(modelName)
noctalia.writeFile(path, "")
noctalia.state.set("llamanager.view", "modelfileEditorView")
end
elseif command == "edit" then
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local path = joinModelNameToPath(modelName)
local content = noctalia.readFile(path)
noctalia.state.set("llamanager.modelfileContent", content)
noctalia.state.set("llamanager.view", "modelfileEditorView")
end
elseif command == "delete" then
-- note: Include the model tag (e.g. :latest, :v2) on deleting a model.
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local base = modelName:gsub(":.*$", "")
local path = modelfilePath .. base .. ".modelfile"
local ok, err = noctalia.removeFile(path)
if not ok then
noctalia.notify("LLamanager", tostring(err))
else
-- delete in ollama
deleteModelOllama(modelName)
end
end
elseif command == "loadToOllama" then
local modelName = noctalia.state.get("llamanager.modelfileNameField")
if isNotEmpty(modelName) then
local path = joinModelNameToPath(modelName)
loadToOllama(modelName, path)
end
end
end)
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+12
View File
@@ -0,0 +1,12 @@
{
"settings": {
"modelfile_path": {
"label": "Modelfile Path",
"description": "The directory where all your modelfiles are stored."
},
"launcher": {
"label": "Launcher",
"description": "Preferred AI model when using /ll in the launcher."
}
}
}
+6
View File
@@ -0,0 +1,6 @@
function onClick()
noctalia.togglePanel("marccvictoria/llamanager:panel")
end
-- main
barWidget.setGlyph("topology-star-3")