1package bot
2
3import (
4 "fmt"
5 "os"
6 "strings"
7
8 "github.com/patriceckhart/zot/packages/provider"
9)
10
11// StatusSnapshot is the small cross-host state bundle rendered for
12// /status replies.
13type StatusSnapshot struct {
14 Provider string
15 Model string
16 CWD string
17 Usage provider.Usage
18 Subscription bool
19 ContextUsed int
20 ContextMax int
21 Busy bool
22 Queued int
23}
24
25// FormatStatus renders the same compact model/usage/cost/context
26// information shown in the TUI status bar, plus the current directory.
27func FormatStatus(s StatusSnapshot) string {
28 providerName := strings.TrimSpace(s.Provider)
29 model := strings.TrimSpace(s.Model)
30 if providerName == "" {
31 providerName = "unknown"
32 }
33 if model == "" {
34 model = "unknown"
35 }
36
37 var stats []string
38 if s.Usage.InputTokens > 0 {
39 stats = append(stats, fmt.Sprintf("↑%s", formatTokens(s.Usage.InputTokens)))
40 }
41 if s.Usage.OutputTokens > 0 {
42 stats = append(stats, fmt.Sprintf("↓%s", formatTokens(s.Usage.OutputTokens)))
43 }
44 if s.Usage.CacheReadTokens > 0 {
45 stats = append(stats, fmt.Sprintf("R%s", formatTokens(s.Usage.CacheReadTokens)))
46 }
47 if s.Usage.CacheWriteTokens > 0 {
48 stats = append(stats, fmt.Sprintf("W%s", formatTokens(s.Usage.CacheWriteTokens)))
49 }
50 if s.Usage.CostUSD > 0 || s.Subscription {
51 cost := fmt.Sprintf("$%.3f", s.Usage.CostUSD)
52 if s.Subscription {
53 cost += " (sub)"
54 }
55 stats = append(stats, cost)
56 }
57 if ctx := contextUsage(s.ContextUsed, s.ContextMax); ctx != "" {
58 stats = append(stats, ctx)
59 }
60
61 line := fmt.Sprintf("(%s) %s", providerName, model)
62 if len(stats) > 0 {
63 line += " " + strings.Join(stats, " ")
64 }
65
66 state := "idle"
67 if s.Busy {
68 state = "working"
69 }
70 lines := []string{line, "state: " + state}
71 if s.Queued > 0 {
72 lines = append(lines, fmt.Sprintf("queued: %d", s.Queued))
73 }
74 if cwd := shortenHome(strings.TrimSpace(s.CWD)); cwd != "" {
75 lines = append(lines, "cwd: "+cwd)
76 }
77 return strings.Join(lines, "\n")
78}
79
80func contextUsage(used, max int) string {
81 if max <= 0 {
82 if used <= 0 {
83 return ""
84 }
85 return formatTokens(used)
86 }
87 pct := float64(used) / float64(max) * 100
88 return fmt.Sprintf("%.1f%%/%s", pct, formatTokens(max))
89}
90
91func formatTokens(n int) string {
92 switch {
93 case n < 0:
94 return "0"
95 case n < 1000:
96 return fmt.Sprintf("%d", n)
97 case n < 10000:
98 return fmt.Sprintf("%.1fk", float64(n)/1000)
99 case n < 1_000_000:
100 return fmt.Sprintf("%dk", (n+500)/1000)
101 case n < 10_000_000:
102 return fmt.Sprintf("%.1fM", float64(n)/1_000_000)
103 default:
104 return fmt.Sprintf("%dM", (n+500_000)/1_000_000)
105 }
106}
107
108func shortenHome(path string) string {
109 home, err := os.UserHomeDir()
110 if err != nil || home == "" {
111 return path
112 }
113 if path == home {
114 return "~"
115 }
116 if strings.HasPrefix(path, home+string(os.PathSeparator)) {
117 return "~" + strings.TrimPrefix(path, home)
118 }
119 return path
120}