Files
llmgw/internal/cli/tui_jobs.go
T
Ilker ManapandClaude Opus 5.5 e12996f7dd Serve and update models in the background from the console, and follow them under Jobs
The console starts a serve or update job over the admin API and stays,
instead of closing to follow the download. Jobs lists them with their
progress and steps; the sidebar counts those running. ctl model serve and
update take --detach; ctl jobs lists jobs and follows one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 01:27:02 +02:00

103 lines
3.0 KiB
Go

package cli
import (
"encoding/json"
"fmt"
"strings"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/modelserve"
)
// Jobs: serving a model or moving one to new weights is a job on the control
// plane — a download that can take an hour, then a replica starting. Started
// from the console, it runs there whatever this screen does; the console is
// free at once, and this view follows it.
// startedSays is the result of starting a job.
func startedSays(raw json.RawMessage) string {
var j modelserve.Job
if json.Unmarshal(raw, &j) != nil || j.ID == "" {
return "Started."
}
return fmt.Sprintf("Started %s. It runs on the control plane — leaving this screen does not stop it.\n"+
"Jobs, under Watch, shows how far it has got.", j.ID)
}
// runningJobs counts the jobs still under way.
func runningJobs(jobs []modelserve.Job) int {
n := 0
for _, j := range jobs {
if j.State == modelserve.Running {
n++
}
}
return n
}
func jobState(j modelserve.Job) string {
switch j.State {
case modelserve.Running:
return sWarn.Render("running")
case modelserve.Failed:
return sBad.Render("failed")
}
for _, st := range j.Steps {
if st.Name == "placement" && st.State == modelserve.Skipped {
return sWarn.Render("waiting for room")
}
}
return sOK.Render("done")
}
// jobNow is the step a job is at, in a line.
func jobNow(j modelserve.Job) string {
if j.State == modelserve.Failed {
return j.Error
}
if len(j.Steps) == 0 {
return ""
}
st := j.Steps[len(j.Steps)-1]
return st.Name + ": " + st.Detail
}
func (m consoleModel) jobsPanel(pw int) (string, string, string) {
keys := "→ or Enter open · ↑↓ choose · Q leave"
if m.inPanel {
keys = "↑↓ row · Enter its steps · ← back · Q leave"
}
head := "\n " + sDim.Render("Models being served or updated, on the control plane; kept until it restarts.") + "\n\n"
var rows [][]string
for _, j := range m.data.jobs {
kind := j.Kind
if kind == "" {
kind = "serve"
}
what := j.Model
if what == "" {
what = j.Request.Repo
}
rows = append(rows, []string{ageShort(j.Started), kind, what, jobState(j), jobNow(j)})
}
if len(rows) == 0 {
return "Jobs", head + " " + sDim.Render("None since the control plane started. Serve a model, under Do, starts one."), keys
}
return "Jobs", head + table([]string{"STARTED", "KIND", "MODEL", "STATE", "NOW"}, rows, m.row, pw, m.inPanel), keys
}
// jobDialog is a job's steps, each as it stands.
func jobDialog(j modelserve.Job) (string, string) {
var b strings.Builder
mark := map[string]string{modelserve.Running: sWarn.Render(".."), modelserve.Done: sOK.Render("ok"),
modelserve.Failed: sBad.Render("!!"), modelserve.Skipped: sDim.Render("--")}
for _, st := range j.Steps {
fmt.Fprintf(&b, "%s %-11s %s\n", mark[st.State], st.Name, wrap(st.Detail, 80, strings.Repeat(" ", 15)))
}
if j.Reply != "" {
fmt.Fprintf(&b, "\n%s said %q at %.1f tokens/s\n", j.Model, j.Reply, j.TokensPerSecond)
}
fmt.Fprintf(&b, "\n%s, started %s\n\n%s", j.ID, j.Started.Format(time.DateTime), sDim.Render("any key"))
return j.ID, b.String()
}