table.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. package logs
  2. import (
  3. "context"
  4. "time"
  5. "github.com/charmbracelet/bubbles/key"
  6. "github.com/charmbracelet/bubbles/table"
  7. tea "github.com/charmbracelet/bubbletea"
  8. "github.com/opencode-ai/opencode/internal/logging"
  9. "github.com/opencode-ai/opencode/internal/pubsub"
  10. "github.com/opencode-ai/opencode/internal/tui/components/chat"
  11. "github.com/opencode-ai/opencode/internal/tui/layout"
  12. "github.com/opencode-ai/opencode/internal/tui/theme"
  13. "github.com/opencode-ai/opencode/internal/tui/util"
  14. )
  15. type TableComponent interface {
  16. tea.Model
  17. layout.Sizeable
  18. layout.Bindings
  19. }
  20. type tableCmp struct {
  21. table table.Model
  22. focused bool
  23. logs []logging.Log
  24. }
  25. type selectedLogMsg logging.Log
  26. // Message for when logs are loaded from the database
  27. type logsLoadedMsg struct {
  28. logs []logging.Log
  29. }
  30. func (i *tableCmp) Init() tea.Cmd {
  31. return i.fetchLogs()
  32. }
  33. func (i *tableCmp) fetchLogs() tea.Cmd {
  34. return func() tea.Msg {
  35. ctx := context.Background()
  36. loggingService := logging.GetService()
  37. if loggingService == nil {
  38. return nil
  39. }
  40. var logs []logging.Log
  41. var err error
  42. sessionId := "" //session.CurrentSessionID()
  43. // Limit the number of logs to improve performance
  44. const logLimit = 100
  45. if sessionId == "" {
  46. logs, err = loggingService.ListAll(ctx, logLimit)
  47. } else {
  48. logs, err = loggingService.ListBySession(ctx, sessionId)
  49. // Trim logs if there are too many
  50. if err == nil && len(logs) > logLimit {
  51. logs = logs[len(logs)-logLimit:]
  52. }
  53. }
  54. if err != nil {
  55. return nil
  56. }
  57. return logsLoadedMsg{logs: logs}
  58. }
  59. }
  60. func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  61. var cmds []tea.Cmd
  62. switch msg := msg.(type) {
  63. case logsLoadedMsg:
  64. i.logs = msg.logs
  65. i.updateRows()
  66. return i, nil
  67. case chat.SessionSelectedMsg:
  68. return i, i.fetchLogs()
  69. case pubsub.Event[logging.Log]:
  70. // Only handle created events
  71. if msg.Type == logging.EventLogCreated {
  72. // Add the new log to our list
  73. i.logs = append([]logging.Log{msg.Payload}, i.logs...)
  74. // Keep the list at a reasonable size
  75. if len(i.logs) > 100 {
  76. i.logs = i.logs[:100]
  77. }
  78. i.updateRows()
  79. }
  80. return i, nil
  81. }
  82. // Only process keyboard input when focused
  83. if _, ok := msg.(tea.KeyMsg); ok && !i.focused {
  84. return i, nil
  85. }
  86. t, cmd := i.table.Update(msg)
  87. cmds = append(cmds, cmd)
  88. i.table = t
  89. // Only send selected log message when selection changes
  90. selectedRow := i.table.SelectedRow()
  91. if selectedRow != nil {
  92. // Use a map for faster lookups by ID
  93. for _, log := range i.logs {
  94. if log.ID == selectedRow[0] {
  95. cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
  96. break
  97. }
  98. }
  99. }
  100. return i, tea.Batch(cmds...)
  101. }
  102. func (i *tableCmp) View() string {
  103. t := theme.CurrentTheme()
  104. defaultStyles := table.DefaultStyles()
  105. defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
  106. i.table.SetStyles(defaultStyles)
  107. return i.table.View()
  108. }
  109. func (i *tableCmp) GetSize() (int, int) {
  110. return i.table.Width(), i.table.Height()
  111. }
  112. func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
  113. i.table.SetWidth(width)
  114. i.table.SetHeight(height)
  115. columns := i.table.Columns()
  116. // Calculate widths for visible columns
  117. timeWidth := 8 // Fixed width for Time column
  118. levelWidth := 7 // Fixed width for Level column
  119. // Message column gets the remaining space
  120. messageWidth := width - timeWidth - levelWidth - 5 // 5 for padding and borders
  121. // Set column widths
  122. columns[0].Width = 0 // ID column (hidden)
  123. columns[1].Width = timeWidth
  124. columns[2].Width = levelWidth
  125. columns[3].Width = messageWidth
  126. i.table.SetColumns(columns)
  127. return nil
  128. }
  129. func (i *tableCmp) BindingKeys() []key.Binding {
  130. return layout.KeyMapToSlice(i.table.KeyMap)
  131. }
  132. func (i *tableCmp) updateRows() {
  133. rows := make([]table.Row, 0, len(i.logs))
  134. // Logs are already sorted by timestamp (newest first) from the database query
  135. // Skip the expensive sort operation
  136. for _, log := range i.logs {
  137. // Format timestamp as time
  138. timeStr := time.Unix(log.Timestamp, 0).Format("15:04:05")
  139. // Include ID as hidden first column for selection
  140. row := table.Row{
  141. log.ID,
  142. timeStr,
  143. log.Level,
  144. log.Message,
  145. }
  146. rows = append(rows, row)
  147. }
  148. i.table.SetRows(rows)
  149. }
  150. func NewLogsTable() TableComponent {
  151. columns := []table.Column{
  152. {Title: "ID", Width: 0}, // ID column with zero width
  153. {Title: "Time", Width: 8},
  154. {Title: "Level", Width: 7},
  155. {Title: "Message", Width: 30},
  156. }
  157. tableModel := table.New(
  158. table.WithColumns(columns),
  159. )
  160. tableModel.Focus()
  161. return &tableCmp{
  162. table: tableModel,
  163. logs: []logging.Log{},
  164. }
  165. }
  166. // Focus implements the focusable interface
  167. func (i *tableCmp) Focus() {
  168. i.focused = true
  169. i.table.Focus()
  170. }
  171. // Blur implements the blurable interface
  172. func (i *tableCmp) Blur() {
  173. i.focused = false
  174. // Table doesn't have a Blur method, but we can implement it here
  175. // to satisfy the interface
  176. }