Vectrum
Sign In
139 lines · 3.2 KB · Go
package tui

import (
	"fmt"
	"net/url"
	"os"
	"strings"
	"unicode"

	"harness/internal/llm"
)

// human renders a token count the way a status bar wants to read it.
func human(n int) string {
	switch {
	case n >= 1_000_000:
		return fmt.Sprintf("%.1fM", float64(n)/1e6)
	case n >= 1_000:
		return fmt.Sprintf("%.1fk", float64(n)/1e3)
	}
	return fmt.Sprint(n)
}

// prettyModel turns an API model id into the name a person uses for it:
// claude-opus-5 becomes "Opus 5", claude-haiku-4-5-20251001 becomes
// "Haiku 4.5". An id that does not fit the shape is returned unchanged, which
// matters because this also runs against gateways serving models we have
// never heard of.
func prettyModel(id string) string {
	s := strings.TrimSuffix(strings.TrimPrefix(id, "claude-"), "-latest")
	// Anything bracketed is a variant tag, not part of the name: opus-5[1m].
	tag := ""
	if i := strings.IndexByte(s, '['); i >= 0 {
		tag, s = s[i:], s[:i]
	}
	parts := strings.Split(s, "-")
	if len(parts) < 2 {
		return id
	}
	// A trailing eight-digit date stamp is a build, not a version.
	if last := parts[len(parts)-1]; len(last) == 8 && allDigits(last) {
		parts = parts[:len(parts)-1]
	}
	family, rest := parts[0], parts[1:]
	if family == "" || !isAlpha(family) {
		return id
	}
	name := strings.ToUpper(family[:1]) + family[1:]
	var version []string
	for _, p := range rest {
		if !allDigits(p) {
			return id
		}
		version = append(version, p)
	}
	if len(version) > 0 {
		name += " " + strings.Join(version, ".")
	}
	return name + tag
}

// endpointLabel names where the prompts are going. The default endpoint gets
// a short name; anything else shows its host, because "somewhere that is not
// Anthropic" is the single most important thing the header can say.
func endpointLabel(c *llm.Client) string {
	if c == nil {
		return ""
	}
	if c.IsDefaultEndpoint() {
		return "anthropic"
	}
	if u, err := url.Parse(c.BaseURL); err == nil && u.Host != "" {
		return u.Host
	}
	return c.BaseURL
}

// camel renders a snake_case tool name as the transcript shows it: read_file
// becomes ReadFile. It is a display transform only — approvals and errors
// still quote the real name.
func camel(name string) string {
	parts := strings.Split(name, "_")
	for i, p := range parts {
		if p == "" {
			continue
		}
		parts[i] = strings.ToUpper(p[:1]) + p[1:]
	}
	return strings.Join(parts, "")
}

func allDigits(s string) bool {
	if s == "" {
		return false
	}
	for _, r := range s {
		if !unicode.IsDigit(r) {
			return false
		}
	}
	return true
}

func isAlpha(s string) bool {
	for _, r := range s {
		if !unicode.IsLetter(r) {
			return false
		}
	}
	return s != ""
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func minInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// shortenHome writes a path the way a person says it. The working root is
// shown in the welcome block, where "~/Work/thing" carries the same
// information as the absolute path in a third of the columns.
func shortenHome(path string) string {
	home, err := os.UserHomeDir()
	if err != nil || home == "" || path == home {
		return path
	}
	if strings.HasPrefix(path, home+string(os.PathSeparator)) {
		return "~" + path[len(home):]
	}
	return path
}