Files
MujicaUI/agent_parts.go
T

294 lines
8.1 KiB
Go

package mujica
import (
"bytes"
"encoding/json"
"fmt"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
"mujicaui/internal"
"mujicaui/theme"
)
// AgentState is an agent or step state; zero is pending.
type AgentState int
const (
AgentPending AgentState = iota
AgentPlanning
AgentRunning
AgentWaiting
AgentDone
AgentFailed
AgentSkipped
)
func (s AgentState) String() string {
switch s {
case AgentPending:
return "pending"
case AgentPlanning:
return "planning"
case AgentRunning:
return "running"
case AgentWaiting:
return "waiting"
case AgentDone:
return "done"
case AgentFailed:
return "failed"
case AgentSkipped:
return "skipped"
}
panic(fmt.Sprintf("mujica: unknown AgentState %d", int(s)))
}
// agentTone returns the color and icon marking s.
func agentTone(k theme.Tokens, s AgentState) (ui.Color, *ui.SVG) {
switch s {
case AgentPlanning:
return k.Info, icons.Must("list")
case AgentRunning:
return k.AccentText, icons.Must("loader-circle")
case AgentWaiting:
return k.Warning, icons.Must("clock")
case AgentDone:
return k.Success, icons.Must("circle-check")
case AgentFailed:
return k.Danger, icons.Must("circle-x")
case AgentSkipped:
return k.TextMuted, icons.Must("ban")
}
return k.TextMuted, icons.Must("circle")
}
// agentMark draws s; running spins unless still.
func agentMark(c *ui.Context, s AgentState, size float32, still bool) *ui.Element {
tone, icon := agentTone(Tokens(c), s)
if s == AgentRunning && !still {
return Spinner(c, SpinnerOptions{Size: size, Color: tone, Label: msg(c, "agent_status.running")})
}
return ui.Icon(c, icon).Size(size, size).TextColor(tone).Shrink(0)
}
// agentWord is the built-in word for s.
func agentWord(c *ui.Context, s AgentState) string { return msg(c, "agent_status."+s.String()) }
// agentDuration formats d like "320 ms" or "4.2 s".
func agentDuration(d time.Duration) string {
if d < 0 {
panic(fmt.Sprintf("mujica: negative duration %v", d))
}
switch {
case d < time.Second:
return fmt.Sprintf("%d ms", d.Milliseconds())
case d < time.Minute:
return fmt.Sprintf("%.1f s", d.Seconds())
case d < time.Hour:
return fmt.Sprintf("%d m %02d s", int(d.Minutes()), int(d.Seconds())%60)
}
return fmt.Sprintf("%d h %02d m", int(d.Hours()), int(d.Minutes())%60)
}
// agentCard is the bordered surface every agent card sits on.
func agentCard(c *ui.Context) *ui.Element {
k := Tokens(c)
u := Density(c).Unit()
return ui.Column(c).Shrink(0).Padding(u*3).Gap(u*2).Radius(theme.CardRadius).
Background(k.Surface).Border(theme.BorderWidth, k.Border)
}
// agentCaption is small muted text.
func agentCaption(c *ui.Context, s string) *ui.Element {
return ui.Text(c, s).FontSize(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).TextMuted)
}
// agentWell styles a sunken monospace well.
func agentWell(c *ui.Context, l *ui.Element, maxHeight float32) *ui.Element {
k := Tokens(c)
u := Density(c).Unit()
return l.Shrink(1).MaxHeight(maxHeight).Padding(u*2, u*3).Radius(theme.ControlRadius).
Background(k.Background).Border(theme.BorderWidth, k.Border).Font(theme.MonoFont).
FontSize(FontSize(c, theme.CaptionSize+1))
}
// agentWellRows builds n rows in a well.
func agentWellRows(c *ui.Context, n int, row func(i int), maxHeight float32) *ui.Element {
return agentWell(c, agentRows(c, n, row), maxHeight)
}
// agentRows builds n scrolling rows, virtualized when many.
func agentRows(c *ui.Context, n int, row func(i int)) *ui.Element {
if n > agentVirtualRows {
return ui.List(c, nil, n, row)
}
return ui.Scroll(c).Children(func() {
for i := range n {
row(i)
}
})
}
// agentVirtualRows is the row count above which wells virtualize.
const agentVirtualRows = 200
// agentCodeCache keeps lines and comment states per text.
type agentCodeCache struct {
language, text string
lines []string
open []bool
}
func (cc *agentCodeCache) update(language, text string) {
if cc.lines != nil && cc.language == language && cc.text == text {
return
}
cc.language, cc.text = language, text
cc.lines = internal.SplitLines(text)
cc.open = make([]bool, len(cc.lines))
block := false
for i, line := range cc.lines {
cc.open[i] = block
_, block = internal.Highlight(language, line, block)
}
}
// agentPrettyJSON indents valid JSON; anything else comes back unchanged.
func agentPrettyJSON(s string) string {
var b bytes.Buffer
if json.Indent(&b, []byte(s), "", " ") != nil {
return s
}
return b.String()
}
// agentTokenColor colors a highlight token from the theme.
func agentTokenColor(k theme.Tokens, kind internal.TokenKind) ui.Color {
switch kind {
case internal.TokKeyword:
return k.AccentText
case internal.TokString:
return k.Success
case internal.TokNumber, internal.TokType:
return k.Info
case internal.TokComment, internal.TokPunct:
return k.TextMuted
}
return k.Text
}
// agentCodeLine builds one highlighted line of language.
func agentCodeLine(c *ui.Context, language, line string, inBlock bool) {
k := Tokens(c)
toks, _ := internal.Highlight(language, line, inBlock)
spans := make([]ui.Span, 0, len(toks))
for _, t := range toks {
spans = append(spans, ui.Span{Text: t.Text, Color: agentTokenColor(k, t.Kind)})
}
if len(spans) == 0 {
spans = append(spans, ui.Span{Text: " "})
}
ui.RichText(c, spans...)
}
// agentCode shows highlighted text in a capped well.
func agentCode(c *ui.Context, language, text string, maxHeight float32) *ui.Element {
holder := ui.Column(c).Shrink(1)
cc := ui.Local(holder, "mujica.agent.code", func() *agentCodeCache { return &agentCodeCache{} })
(*cc).update(language, text)
lines, open := (*cc).lines, (*cc).open
row := func(i int) { agentCodeLine(c, language, lines[i], open[i]) }
holder.Children(func() {
agentWellRows(c, len(lines), row, maxHeight)
})
return holder
}
// agentAnsiColor maps a terminal color onto the theme.
func agentAnsiColor(k theme.Tokens, a internal.AnsiColor, fallback ui.Color) ui.Color {
switch a.Kind {
case internal.AnsiRGB:
return ui.RGB(a.R, a.G, a.B)
case internal.AnsiIndexed:
switch a.Index % 8 {
case 0:
return k.TextMuted
case 1:
return k.Danger
case 2:
return k.Success
case 3:
return k.Warning
case 4, 6:
return k.Info
case 5:
return k.AccentText
case 7:
return k.Text
}
}
return fallback
}
// agentAnsiSpans turns parsed terminal spans into rich text spans.
func agentAnsiSpans(k theme.Tokens, in []internal.AnsiSpan) []ui.Span {
out := make([]ui.Span, 0, len(in)+1)
for _, s := range in {
fg := agentAnsiColor(k, s.Style.FG, k.Text)
bg := agentAnsiColor(k, s.Style.BG, ui.Transparent)
if s.Style.Inverse {
fg, bg = agentAnsiColor(k, s.Style.BG, k.Background), fg
}
if s.Style.Dim {
fg = fg.Alpha(0.7)
}
sp := ui.Span{Text: s.Text, Color: fg, Background: bg, Italic: s.Style.Italic,
Underline: s.Style.Underline, Strikethrough: s.Style.Strike}
if s.Style.Bold {
sp.Weight = 700
}
out = append(out, sp)
}
if len(out) == 0 {
out = append(out, ui.Span{Text: " "})
}
return out
}
// agentBar is a thin progress bar named label.
func agentBar(c *ui.Context, frac float64, label string) *ui.Element {
if frac < 0 || frac > 1 || frac != frac {
panic(fmt.Sprintf("mujica: progress %v outside 0..1", frac))
}
k := Tokens(c)
return ui.Box(c).FillWidth().Height(4).Shrink(0).Role(ui.RoleProgress).Label(label).
Draw(func(p *ui.Painter, r ui.Rect) {
p.Fill(r, k.Border, 2)
if frac > 0 {
p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * float32(frac), H: r.H}, k.AccentText, 2)
}
})
}
// agentToggle is a small text button reporting a click.
func agentToggle(c *ui.Context, label string, icon string, disabled bool) bool {
k := Tokens(c)
u := Density(c).Unit()
b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, u*2).Gap(u).
Radius(theme.ControlRadius).Disabled(disabled).TextColor(k.AccentText)
internal.Ring(b, k.Focus, theme.ControlRadius)
if !disabled && b.Hovered() {
b.Background(k.SurfaceHover)
}
b.Children(func() {
if icon != "" {
ui.Icon(c, icons.Must(icon)).Size(14, 14)
}
ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine()
})
return !b.IsDisabled() && b.Clicked()
}