status.go 3.6 KB

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