summaryrefslogtreecommitdiff
path: root/internal/tmux
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-17 21:33:45 +0300
committerPaul Buetow <paul@buetow.org>2025-09-17 21:33:45 +0300
commit88103657fb230bb41217a06aa5602ae23e7acb8b (patch)
tree524c437e4e40ee5d6713b6ea5414ad975654cc52 /internal/tmux
parent2b6232704ecc90630196b9f829f966533e5cdccd (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.go96
-rw-r--r--internal/tmux/status_more_test.go62
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
+}