status.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. package core
  2. import (
  3. "fmt"
  4. "strings"
  5. "time"
  6. tea "github.com/charmbracelet/bubbletea"
  7. "github.com/charmbracelet/lipgloss"
  8. "github.com/kujtimiihoxha/opencode/internal/config"
  9. "github.com/kujtimiihoxha/opencode/internal/llm/models"
  10. "github.com/kujtimiihoxha/opencode/internal/lsp"
  11. "github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
  12. "github.com/kujtimiihoxha/opencode/internal/pubsub"
  13. "github.com/kujtimiihoxha/opencode/internal/session"
  14. "github.com/kujtimiihoxha/opencode/internal/tui/components/chat"
  15. "github.com/kujtimiihoxha/opencode/internal/tui/styles"
  16. "github.com/kujtimiihoxha/opencode/internal/tui/util"
  17. )
  18. type statusCmp struct {
  19. info util.InfoMsg
  20. width int
  21. messageTTL time.Duration
  22. lspClients map[string]*lsp.Client
  23. session session.Session
  24. }
  25. // clearMessageCmd is a command that clears status messages after a timeout
  26. func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd {
  27. return tea.Tick(ttl, func(time.Time) tea.Msg {
  28. return util.ClearStatusMsg{}
  29. })
  30. }
  31. func (m statusCmp) Init() tea.Cmd {
  32. return nil
  33. }
  34. func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  35. switch msg := msg.(type) {
  36. case tea.WindowSizeMsg:
  37. m.width = msg.Width
  38. return m, nil
  39. case chat.SessionSelectedMsg:
  40. m.session = msg
  41. case chat.SessionClearedMsg:
  42. m.session = session.Session{}
  43. case pubsub.Event[session.Session]:
  44. if msg.Type == pubsub.UpdatedEvent {
  45. if m.session.ID == msg.Payload.ID {
  46. m.session = msg.Payload
  47. }
  48. }
  49. case util.InfoMsg:
  50. m.info = msg
  51. ttl := msg.TTL
  52. if ttl == 0 {
  53. ttl = m.messageTTL
  54. }
  55. return m, m.clearMessageCmd(ttl)
  56. case util.ClearStatusMsg:
  57. m.info = util.InfoMsg{}
  58. }
  59. return m, nil
  60. }
  61. var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
  62. func formatTokensAndCost(tokens int64, cost float64) string {
  63. // Format tokens in human-readable format (e.g., 110K, 1.2M)
  64. var formattedTokens string
  65. switch {
  66. case tokens >= 1_000_000:
  67. formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
  68. case tokens >= 1_000:
  69. formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
  70. default:
  71. formattedTokens = fmt.Sprintf("%d", tokens)
  72. }
  73. // Remove .0 suffix if present
  74. if strings.HasSuffix(formattedTokens, ".0K") {
  75. formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
  76. }
  77. if strings.HasSuffix(formattedTokens, ".0M") {
  78. formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
  79. }
  80. // Format cost with $ symbol and 2 decimal places
  81. formattedCost := fmt.Sprintf("$%.2f", cost)
  82. return fmt.Sprintf("Tokens: %s, Cost: %s", formattedTokens, formattedCost)
  83. }
  84. func (m statusCmp) View() string {
  85. status := helpWidget
  86. if m.session.ID != "" {
  87. tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
  88. tokensStyle := styles.Padded.
  89. Background(styles.Forground).
  90. Foreground(styles.BackgroundDim).
  91. Render(tokens)
  92. status += tokensStyle
  93. }
  94. diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
  95. if m.info.Msg != "" {
  96. infoStyle := styles.Padded.
  97. Foreground(styles.Base).
  98. Width(m.availableFooterMsgWidth(diagnostics))
  99. switch m.info.Type {
  100. case util.InfoTypeInfo:
  101. infoStyle = infoStyle.Background(styles.BorderColor)
  102. case util.InfoTypeWarn:
  103. infoStyle = infoStyle.Background(styles.Peach)
  104. case util.InfoTypeError:
  105. infoStyle = infoStyle.Background(styles.Red)
  106. }
  107. // Truncate message if it's longer than available width
  108. msg := m.info.Msg
  109. availWidth := m.availableFooterMsgWidth(diagnostics) - 10
  110. if len(msg) > availWidth && availWidth > 0 {
  111. msg = msg[:availWidth] + "..."
  112. }
  113. status += infoStyle.Render(msg)
  114. } else {
  115. status += styles.Padded.
  116. Foreground(styles.Base).
  117. Background(styles.BackgroundDim).
  118. Width(m.availableFooterMsgWidth(diagnostics)).
  119. Render("")
  120. }
  121. status += diagnostics
  122. status += m.model()
  123. return status
  124. }
  125. func (m *statusCmp) projectDiagnostics() string {
  126. errorDiagnostics := []protocol.Diagnostic{}
  127. warnDiagnostics := []protocol.Diagnostic{}
  128. hintDiagnostics := []protocol.Diagnostic{}
  129. infoDiagnostics := []protocol.Diagnostic{}
  130. for _, client := range m.lspClients {
  131. for _, d := range client.GetDiagnostics() {
  132. for _, diag := range d {
  133. switch diag.Severity {
  134. case protocol.SeverityError:
  135. errorDiagnostics = append(errorDiagnostics, diag)
  136. case protocol.SeverityWarning:
  137. warnDiagnostics = append(warnDiagnostics, diag)
  138. case protocol.SeverityHint:
  139. hintDiagnostics = append(hintDiagnostics, diag)
  140. case protocol.SeverityInformation:
  141. infoDiagnostics = append(infoDiagnostics, diag)
  142. }
  143. }
  144. }
  145. }
  146. if len(errorDiagnostics) == 0 && len(warnDiagnostics) == 0 && len(hintDiagnostics) == 0 && len(infoDiagnostics) == 0 {
  147. return "No diagnostics"
  148. }
  149. diagnostics := []string{}
  150. if len(errorDiagnostics) > 0 {
  151. errStr := lipgloss.NewStyle().
  152. Background(styles.BackgroundDarker).
  153. Foreground(styles.Error).
  154. Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
  155. diagnostics = append(diagnostics, errStr)
  156. }
  157. if len(warnDiagnostics) > 0 {
  158. warnStr := lipgloss.NewStyle().
  159. Background(styles.BackgroundDarker).
  160. Foreground(styles.Warning).
  161. Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
  162. diagnostics = append(diagnostics, warnStr)
  163. }
  164. if len(hintDiagnostics) > 0 {
  165. hintStr := lipgloss.NewStyle().
  166. Background(styles.BackgroundDarker).
  167. Foreground(styles.Text).
  168. Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
  169. diagnostics = append(diagnostics, hintStr)
  170. }
  171. if len(infoDiagnostics) > 0 {
  172. infoStr := lipgloss.NewStyle().
  173. Background(styles.BackgroundDarker).
  174. Foreground(styles.Peach).
  175. Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
  176. diagnostics = append(diagnostics, infoStr)
  177. }
  178. return strings.Join(diagnostics, " ")
  179. }
  180. func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
  181. tokens := ""
  182. tokensWidth := 0
  183. if m.session.ID != "" {
  184. tokens = formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
  185. tokensWidth = lipgloss.Width(tokens) + 2
  186. }
  187. return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-tokensWidth)
  188. }
  189. func (m statusCmp) model() string {
  190. cfg := config.Get()
  191. coder, ok := cfg.Agents[config.AgentCoder]
  192. if !ok {
  193. return "Unknown"
  194. }
  195. model := models.SupportedModels[coder.Model]
  196. return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
  197. }
  198. func NewStatusCmp(lspClients map[string]*lsp.Client) tea.Model {
  199. return &statusCmp{
  200. messageTTL: 10 * time.Second,
  201. lspClients: lspClients,
  202. }
  203. }