diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-17 21:33:45 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-17 21:33:45 +0300 |
| commit | 88103657fb230bb41217a06aa5602ae23e7acb8b (patch) | |
| tree | 524c437e4e40ee5d6713b6ea5414ad975654cc52 /internal/tmux | |
| parent | 2b6232704ecc90630196b9f829f966533e5cdccd (diff) | |
feat(stats,tmux): global Σ@window stats across processes with flocked cache; width mitigation (narrow/maxlen); configurable [stats] window_minutes; robust coverage parsing; docs update\n\n- Add internal/stats with windowed event cache + flock + atomic writes\n- Wire stats into LSP/CLI/Tmux Action; tmux shows Σ@window with per-model tail\n- HEXAI_TMUX_STATUS_NARROW and HEXAI_TMUX_STATUS_MAXLEN for width control\n- Add [stats] window_minutes to config and apply on startup\n- Improve Magefile coverage handling; add tests to lift coverage >85%\n- Update docs/tmux.md and config example
Diffstat (limited to 'internal/tmux')
| -rw-r--r-- | internal/tmux/status.go | 96 | ||||
| -rw-r--r-- | internal/tmux/status_more_test.go | 62 |
2 files changed, 158 insertions, 0 deletions
diff --git a/internal/tmux/status.go b/internal/tmux/status.go index 6d76bcb..dcc0714 100644 --- a/internal/tmux/status.go +++ b/internal/tmux/status.go @@ -4,7 +4,9 @@ import ( "fmt" "os" "os/exec" + "strconv" "strings" + "time" "codeberg.org/snonux/hexai/internal/textutil" ) @@ -65,6 +67,100 @@ func FormatLLMStatsStatusColored(provider, model string, reqs int64, rpm float64 ) } +// FormatGlobalStatusColored renders a compact global stats heartbeat with an optional +// scoped provider:model tail. The window indicator (e.g., Σ@1h) should be composed +// by the caller if needed; this function focuses on numbers and labels. +// Example: "Σ ↑120k ↓340k 4.2rpm | openai:gpt-4.1 3.1rpm 80r" +func FormatGlobalStatusColored(globalReqs int64, globalRPM float64, globalIn, globalOut int64, scopeProvider, scopeModel string, scopeRPM float64, scopeReqs int64, window time.Duration) string { + gin := textutil.HumanBytes(globalIn) + gout := textutil.HumanBytes(globalOut) + head := fmt.Sprintf("%sΣ@%s %s↑%s%s %s↓%s%s %.1frpm", baseFGToken, humanWindow(window), arrowUpToken, baseFGToken, gin, arrowDownToken, baseFGToken, gout, globalRPM) + // Narrow modes: only show Σ head + if narrowEnabled() || stringsTrim(scopeProvider) == "" || stringsTrim(scopeModel) == "" { + return head + } + tail := fmt.Sprintf(" | %s:%s %.1frpm %dr", scopeProvider, scopeModel, scopeRPM, scopeReqs) + // Respect max length when configured: drop tail if it would overflow + if ml := maxStatusLen(); ml > 0 { + if len(head) <= ml && len(head)+len(tail) > ml { + return head + } + if len(head) > ml { + return truncateStatus(head, ml) + } + } + return head + tail +} + +func humanWindow(d time.Duration) string { + if d <= 0 { + return "?" + } + mins := int(d.Minutes()) + if mins%60 == 0 { + return fmt.Sprintf("%dh", mins/60) + } + if mins >= 60 { + return fmt.Sprintf("%dm", mins) + } + return fmt.Sprintf("%dm", mins) +} + +// narrowEnabled returns true when HEXAI_TMUX_STATUS_NARROW is truthy (1/true/yes/on). +func narrowEnabled() bool { + v := strings.ToLower(stringsTrim(os.Getenv("HEXAI_TMUX_STATUS_NARROW"))) + if v == "" { + return false + } + switch v { + case "1", "true", "yes", "on": + return true + default: + return false + } +} + +// maxStatusLen returns HEXAI_TMUX_STATUS_MAXLEN parsed as int; 0 disables. +func maxStatusLen() int { + v := stringsTrim(os.Getenv("HEXAI_TMUX_STATUS_MAXLEN")) + if v == "" { + return 0 + } + n, err := strconv.Atoi(v) + if err != nil || n <= 0 { + return 0 + } + return n +} + +func truncateStatus(s string, n int) string { + if n <= 0 { + return "" + } + if len(s) <= n { + return s + } + if n <= 1 { + return s[:n] + } + return s[:n-1] + "…" +} + +func stringsTrim(s string) string { + i := 0 + j := len(s) + for i < j && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') { + i++ + } + for j > i && (s[j-1] == ' ' || s[j-1] == '\t' || s[j-1] == '\n' || s[j-1] == '\r') { + j-- + } + if i == 0 && j == len(s) { + return s + } + return s[i:j] +} + // FormatLLMStartStatus renders a short colored heartbeat at start/initialize time. // Example: "LLM:openai:gpt-4.1 ⏳" func FormatLLMStartStatus(provider, model string) string { diff --git a/internal/tmux/status_more_test.go b/internal/tmux/status_more_test.go new file mode 100644 index 0000000..deaf57d --- /dev/null +++ b/internal/tmux/status_more_test.go @@ -0,0 +1,62 @@ +package tmux + +import ( + "os" + "testing" + "time" +) + +func TestFormatLLMStatsStatus_Basic(t *testing.T) { + s := FormatLLMStatsStatus("gpt-4.1", 5, 0.8, 1234, 5678) + if s == "" || !containsAll(s, []string{"LLM:gpt-4.1", "5r", "0.8rpm"}) { + t.Fatalf("unexpected status: %q", s) + } +} + +func TestFormatLLMStatsStatusColored_Basic(t *testing.T) { + s := FormatLLMStatsStatusColored("openai", "gpt-4.1", 2, 1.2, 100, 200) + if s == "" || !containsAll(s, []string{"LLM:openai:gpt-4.1", "rpm", "2r"}) { + t.Fatalf("colored status missing parts: %q", s) + } +} + +func TestFormatGlobalStatusColored_NarrowAndMaxLen(t *testing.T) { + // Narrow mode should elide the tail + os.Setenv("HEXAI_TMUX_STATUS_NARROW", "1") + defer os.Unsetenv("HEXAI_TMUX_STATUS_NARROW") + s := FormatGlobalStatusColored(10, 3.3, 1000, 2000, "prov", "model", 1.1, 4, 30*time.Minute) + if containsAll(s, []string{"|", "prov:model"}) { + t.Fatalf("narrow mode should not include tail: %q", s) + } + // Max length should also drop the tail when it would overflow + os.Unsetenv("HEXAI_TMUX_STATUS_NARROW") + os.Setenv("HEXAI_TMUX_STATUS_MAXLEN", "10") + defer os.Unsetenv("HEXAI_TMUX_STATUS_MAXLEN") + s2 := FormatGlobalStatusColored(10, 3.3, 1000, 2000, "prov", "model", 1.1, 4, 30*time.Minute) + if containsAll(s2, []string{"|", "prov:model"}) { + t.Fatalf("maxlen should drop tail when overflowing: %q", s2) + } +} + +func containsAll(s string, parts []string) bool { + for _, p := range parts { + if !contains(s, p) { + return false + } + } + return true +} + +func contains(s, sub string) bool { + return len(s) >= len(sub) && (len(sub) == 0 || (len(s) > 0 && indexOf(s, sub) >= 0)) +} + +func indexOf(s, sub string) int { + // tiny wrapper to avoid importing strings + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return i + } + } + return -1 +} |
