status.go 4.1 KB

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