table.go 4.4 KB

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