status.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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/opencode-ai/opencode/internal/config"
  9. "github.com/opencode-ai/opencode/internal/llm/models"
  10. "github.com/opencode-ai/opencode/internal/lsp"
  11. "github.com/opencode-ai/opencode/internal/lsp/protocol"
  12. "github.com/opencode-ai/opencode/internal/pubsub"
  13. "github.com/opencode-ai/opencode/internal/session"
  14. "github.com/opencode-ai/opencode/internal/tui/components/chat"
  15. "github.com/opencode-ai/opencode/internal/tui/styles"
  16. "github.com/opencode-ai/opencode/internal/tui/theme"
  17. "github.com/opencode-ai/opencode/internal/tui/util"
  18. )
  19. type StatusCmp interface {
  20. tea.Model
  21. SetHelpMsg(string)
  22. }
  23. type statusCmp struct {
  24. info util.InfoMsg
  25. width int
  26. messageTTL time.Duration
  27. lspClients map[string]*lsp.Client
  28. session session.Session
  29. }
  30. // clearMessageCmd is a command that clears status messages after a timeout
  31. func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd {
  32. return tea.Tick(ttl, func(time.Time) tea.Msg {
  33. return util.ClearStatusMsg{}
  34. })
  35. }
  36. func (m statusCmp) Init() tea.Cmd {
  37. return nil
  38. }
  39. func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  40. switch msg := msg.(type) {
  41. case tea.WindowSizeMsg:
  42. m.width = msg.Width
  43. return m, nil
  44. case chat.SessionSelectedMsg:
  45. m.session = msg
  46. case chat.SessionClearedMsg:
  47. m.session = session.Session{}
  48. case pubsub.Event[session.Session]:
  49. if msg.Type == pubsub.UpdatedEvent {
  50. if m.session.ID == msg.Payload.ID {
  51. m.session = msg.Payload
  52. }
  53. }
  54. case util.InfoMsg:
  55. m.info = msg
  56. ttl := msg.TTL
  57. if ttl == 0 {
  58. ttl = m.messageTTL
  59. }
  60. return m, m.clearMessageCmd(ttl)
  61. case util.ClearStatusMsg:
  62. m.info = util.InfoMsg{}
  63. }
  64. return m, nil
  65. }
  66. var helpWidget = ""
  67. // getHelpWidget returns the help widget with current theme colors
  68. func getHelpWidget(helpText string) string {
  69. t := theme.CurrentTheme()
  70. if helpText == "" {
  71. helpText = "ctrl+? help"
  72. }
  73. return styles.Padded().
  74. Background(t.TextMuted()).
  75. Foreground(t.BackgroundDarker()).
  76. Bold(true).
  77. Render(helpText)
  78. }
  79. func formatTokensAndCost(tokens int64, cost float64) string {
  80. // Format tokens in human-readable format (e.g., 110K, 1.2M)
  81. var formattedTokens string
  82. switch {
  83. case tokens >= 1_000_000:
  84. formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
  85. case tokens >= 1_000:
  86. formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
  87. default:
  88. formattedTokens = fmt.Sprintf("%d", tokens)
  89. }
  90. // Remove .0 suffix if present
  91. if strings.HasSuffix(formattedTokens, ".0K") {
  92. formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
  93. }
  94. if strings.HasSuffix(formattedTokens, ".0M") {
  95. formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
  96. }
  97. // Format cost with $ symbol and 2 decimal places
  98. formattedCost := fmt.Sprintf("$%.2f", cost)
  99. return fmt.Sprintf("Tokens: %s, Cost: %s", formattedTokens, formattedCost)
  100. }
  101. func (m statusCmp) View() string {
  102. t := theme.CurrentTheme()
  103. // Initialize the help widget
  104. status := getHelpWidget("")
  105. if m.session.ID != "" {
  106. tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
  107. tokensStyle := styles.Padded().
  108. Background(t.Text()).
  109. Foreground(t.BackgroundSecondary()).
  110. Render(tokens)
  111. status += tokensStyle
  112. }
  113. diagnostics := styles.Padded().
  114. Background(t.BackgroundDarker()).
  115. Render(m.projectDiagnostics())
  116. if m.info.Msg != "" {
  117. infoStyle := styles.Padded().
  118. Foreground(t.Background()).
  119. Width(m.availableFooterMsgWidth(diagnostics))
  120. switch m.info.Type {
  121. case util.InfoTypeInfo:
  122. infoStyle = infoStyle.Background(t.Info())
  123. case util.InfoTypeWarn:
  124. infoStyle = infoStyle.Background(t.Warning())
  125. case util.InfoTypeError:
  126. infoStyle = infoStyle.Background(t.Error())
  127. }
  128. // Truncate message if it's longer than available width
  129. msg := m.info.Msg
  130. availWidth := m.availableFooterMsgWidth(diagnostics) - 10
  131. if len(msg) > availWidth && availWidth > 0 {
  132. msg = msg[:availWidth] + "..."
  133. }
  134. status += infoStyle.Render(msg)
  135. } else {
  136. status += styles.Padded().
  137. Foreground(t.Text()).
  138. Background(t.BackgroundSecondary()).
  139. Width(m.availableFooterMsgWidth(diagnostics)).
  140. Render("")
  141. }
  142. status += diagnostics
  143. status += m.model()
  144. return status
  145. }
  146. func (m *statusCmp) projectDiagnostics() string {
  147. t := theme.CurrentTheme()
  148. // Check if any LSP server is still initializing
  149. initializing := false
  150. for _, client := range m.lspClients {
  151. if client.GetServerState() == lsp.StateStarting {
  152. initializing = true
  153. break
  154. }
  155. }
  156. // If any server is initializing, show that status
  157. if initializing {
  158. return lipgloss.NewStyle().
  159. Background(t.BackgroundDarker()).
  160. Foreground(t.Warning()).
  161. Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
  162. }
  163. errorDiagnostics := []protocol.Diagnostic{}
  164. warnDiagnostics := []protocol.Diagnostic{}
  165. hintDiagnostics := []protocol.Diagnostic{}
  166. infoDiagnostics := []protocol.Diagnostic{}
  167. for _, client := range m.lspClients {
  168. for _, d := range client.GetDiagnostics() {
  169. for _, diag := range d {
  170. switch diag.Severity {
  171. case protocol.SeverityError:
  172. errorDiagnostics = append(errorDiagnostics, diag)
  173. case protocol.SeverityWarning:
  174. warnDiagnostics = append(warnDiagnostics, diag)
  175. case protocol.SeverityHint:
  176. hintDiagnostics = append(hintDiagnostics, diag)
  177. case protocol.SeverityInformation:
  178. infoDiagnostics = append(infoDiagnostics, diag)
  179. }
  180. }
  181. }
  182. }
  183. if len(errorDiagnostics) == 0 && len(warnDiagnostics) == 0 && len(hintDiagnostics) == 0 && len(infoDiagnostics) == 0 {
  184. return "No diagnostics"
  185. }
  186. diagnostics := []string{}
  187. if len(errorDiagnostics) > 0 {
  188. errStr := lipgloss.NewStyle().
  189. Background(t.BackgroundDarker()).
  190. Foreground(t.Error()).
  191. Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
  192. diagnostics = append(diagnostics, errStr)
  193. }
  194. if len(warnDiagnostics) > 0 {
  195. warnStr := lipgloss.NewStyle().
  196. Background(t.BackgroundDarker()).
  197. Foreground(t.Warning()).
  198. Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
  199. diagnostics = append(diagnostics, warnStr)
  200. }
  201. if len(hintDiagnostics) > 0 {
  202. hintStr := lipgloss.NewStyle().
  203. Background(t.BackgroundDarker()).
  204. Foreground(t.Text()).
  205. Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
  206. diagnostics = append(diagnostics, hintStr)
  207. }
  208. if len(infoDiagnostics) > 0 {
  209. infoStr := lipgloss.NewStyle().
  210. Background(t.BackgroundDarker()).
  211. Foreground(t.Info()).
  212. Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
  213. diagnostics = append(diagnostics, infoStr)
  214. }
  215. return strings.Join(diagnostics, " ")
  216. }
  217. func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
  218. tokens := ""
  219. tokensWidth := 0
  220. if m.session.ID != "" {
  221. tokens = formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
  222. tokensWidth = lipgloss.Width(tokens) + 2
  223. }
  224. return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-tokensWidth)
  225. }
  226. func (m statusCmp) model() string {
  227. t := theme.CurrentTheme()
  228. cfg := config.Get()
  229. coder, ok := cfg.Agents[config.AgentCoder]
  230. if !ok {
  231. return "Unknown"
  232. }
  233. model := models.SupportedModels[coder.Model]
  234. return styles.Padded().
  235. Background(t.Secondary()).
  236. Foreground(t.Background()).
  237. Render(model.Name)
  238. }
  239. func (m statusCmp) SetHelpMsg(s string) {
  240. // Update the help widget text using the getHelpWidget function
  241. helpWidget = getHelpWidget(s)
  242. }
  243. func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
  244. // Initialize the help widget with default text
  245. helpWidget = getHelpWidget("")
  246. return &statusCmp{
  247. messageTTL: 10 * time.Second,
  248. lspClients: lspClients,
  249. }
  250. }