status.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package core
  2. import (
  3. "fmt"
  4. "strings"
  5. tea "github.com/charmbracelet/bubbletea/v2"
  6. "github.com/charmbracelet/lipgloss/v2"
  7. "github.com/sst/opencode/internal/app"
  8. "github.com/sst/opencode/internal/layout"
  9. "github.com/sst/opencode/internal/styles"
  10. "github.com/sst/opencode/internal/theme"
  11. )
  12. type StatusComponent interface {
  13. layout.ModelWithView
  14. }
  15. type statusComponent struct {
  16. app *app.App
  17. width int
  18. }
  19. func (m statusComponent) Init() tea.Cmd {
  20. return nil
  21. }
  22. func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  23. switch msg := msg.(type) {
  24. case tea.WindowSizeMsg:
  25. m.width = msg.Width
  26. return m, nil
  27. }
  28. return m, nil
  29. }
  30. func (m statusComponent) logo() string {
  31. t := theme.CurrentTheme()
  32. base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
  33. emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
  34. open := base("open")
  35. code := emphasis("code ")
  36. version := base(m.app.Version)
  37. return styles.Padded().
  38. Background(t.BackgroundElement()).
  39. Render(open + code + version)
  40. }
  41. func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string {
  42. // Format tokens in human-readable format (e.g., 110K, 1.2M)
  43. var formattedTokens string
  44. switch {
  45. case tokens >= 1_000_000:
  46. formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
  47. case tokens >= 1_000:
  48. formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
  49. default:
  50. formattedTokens = fmt.Sprintf("%d", int(tokens))
  51. }
  52. // Remove .0 suffix if present
  53. if strings.HasSuffix(formattedTokens, ".0K") {
  54. formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
  55. }
  56. if strings.HasSuffix(formattedTokens, ".0M") {
  57. formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
  58. }
  59. // Format cost with $ symbol and 2 decimal places
  60. formattedCost := fmt.Sprintf("$%.2f", cost)
  61. percentage := (float64(tokens) / float64(contextWindow)) * 100
  62. return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
  63. }
  64. func (m statusComponent) View() string {
  65. t := theme.CurrentTheme()
  66. if m.app.Session.Id == "" {
  67. return styles.BaseStyle().
  68. Background(t.Background()).
  69. Width(m.width).
  70. Height(2).
  71. Render("")
  72. }
  73. logo := m.logo()
  74. cwd := styles.Padded().
  75. Foreground(t.TextMuted()).
  76. Background(t.BackgroundSubtle()).
  77. Render(m.app.Info.Path.Cwd)
  78. sessionInfo := ""
  79. if m.app.Session.Id != "" {
  80. tokens := float32(0)
  81. cost := float32(0)
  82. contextWindow := m.app.Model.Limit.Context
  83. for _, message := range m.app.Messages {
  84. if message.Metadata.Assistant != nil {
  85. cost += message.Metadata.Assistant.Cost
  86. usage := message.Metadata.Assistant.Tokens
  87. if usage.Output > 0 {
  88. tokens = (usage.Input +
  89. usage.Cache.Write +
  90. usage.Cache.Read +
  91. usage.Output +
  92. usage.Reasoning)
  93. }
  94. }
  95. }
  96. sessionInfo = styles.Padded().
  97. Background(t.BackgroundElement()).
  98. Foreground(t.TextMuted()).
  99. Render(formatTokensAndCost(tokens, contextWindow, cost))
  100. }
  101. // diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics())
  102. space := max(
  103. 0,
  104. m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
  105. )
  106. spacer := lipgloss.NewStyle().Background(t.BackgroundSubtle()).Width(space).Render("")
  107. status := logo + cwd + spacer + sessionInfo
  108. blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
  109. return blank + "\n" + status
  110. }
  111. func NewStatusCmp(app *app.App) StatusComponent {
  112. statusComponent := &statusComponent{
  113. app: app,
  114. }
  115. return statusComponent
  116. }