table.go 4.5 KB

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