| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- package logs
- import (
- "context"
- "log/slog"
- "github.com/charmbracelet/bubbles/key"
- "github.com/charmbracelet/bubbles/table"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/sst/opencode/internal/app"
- "github.com/sst/opencode/internal/logging"
- "github.com/sst/opencode/internal/pubsub"
- "github.com/sst/opencode/internal/tui/layout"
- "github.com/sst/opencode/internal/tui/state"
- "github.com/sst/opencode/internal/tui/theme"
- )
- type TableComponent interface {
- tea.Model
- layout.Sizeable
- layout.Bindings
- }
- type tableCmp struct {
- app *app.App
- table table.Model
- focused bool
- logs []logging.Log
- selectedLogID string
- }
- type selectedLogMsg logging.Log
- type LogsLoadedMsg struct {
- logs []logging.Log
- }
- func (i *tableCmp) Init() tea.Cmd {
- return i.fetchLogs()
- }
- func (i *tableCmp) fetchLogs() tea.Cmd {
- return func() tea.Msg {
- ctx := context.Background()
- var logs []logging.Log
- var err error
- // Limit the number of logs to improve performance
- const logLimit = 100
- if i.app.CurrentSession.ID == "" {
- logs, err = i.app.Logs.ListAll(ctx, logLimit)
- } else {
- logs, err = i.app.Logs.ListBySession(ctx, i.app.CurrentSession.ID)
- }
- if err != nil {
- slog.Error("Failed to fetch logs", "error", err)
- return nil
- }
- return LogsLoadedMsg{logs: logs}
- }
- }
- func (i *tableCmp) updateRows() tea.Cmd {
- return func() tea.Msg {
- rows := make([]table.Row, 0, len(i.logs))
- for _, log := range i.logs {
- timeStr := log.Timestamp.Local().Format("15:04:05")
- // Include ID as hidden first column for selection
- row := table.Row{
- log.ID,
- timeStr,
- log.Level,
- log.Message,
- }
- rows = append(rows, row)
- }
- i.table.SetRows(rows)
- return nil
- }
- }
- func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- switch msg := msg.(type) {
- case LogsLoadedMsg:
- i.logs = msg.logs
- return i, i.updateRows()
- case state.SessionSelectedMsg:
- return i, i.fetchLogs()
- case pubsub.Event[logging.Log]:
- if msg.Type == logging.EventLogCreated {
- // Add the new log to our list
- i.logs = append([]logging.Log{msg.Payload}, i.logs...)
- // Keep the list at a reasonable size
- if len(i.logs) > 100 {
- i.logs = i.logs[:100]
- }
- return i, i.updateRows()
- }
- return i, nil
- }
- // Only process keyboard input when focused
- if _, ok := msg.(tea.KeyMsg); ok && !i.focused {
- return i, nil
- }
- t, cmd := i.table.Update(msg)
- cmds = append(cmds, cmd)
- i.table = t
- selectedRow := i.table.SelectedRow()
- if selectedRow != nil {
- // Only send message if it's a new selection
- if i.selectedLogID != selectedRow[0] {
- cmds = append(cmds, func() tea.Msg {
- for _, log := range i.logs {
- if log.ID == selectedRow[0] {
- return selectedLogMsg(log)
- }
- }
- return nil
- })
- }
- i.selectedLogID = selectedRow[0]
- }
- return i, tea.Batch(cmds...)
- }
- func (i *tableCmp) View() string {
- t := theme.CurrentTheme()
- defaultStyles := table.DefaultStyles()
- defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
- i.table.SetStyles(defaultStyles)
- return i.table.View()
- }
- func (i *tableCmp) GetSize() (int, int) {
- return i.table.Width(), i.table.Height()
- }
- func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
- i.table.SetWidth(width)
- i.table.SetHeight(height)
- columns := i.table.Columns()
- // Calculate widths for visible columns
- timeWidth := 8 // Fixed width for Time column
- levelWidth := 7 // Fixed width for Level column
- // Message column gets the remaining space
- messageWidth := width - timeWidth - levelWidth - 5 // 5 for padding and borders
- // Set column widths
- columns[0].Width = 0 // ID column (hidden)
- columns[1].Width = timeWidth
- columns[2].Width = levelWidth
- columns[3].Width = messageWidth
- i.table.SetColumns(columns)
- return nil
- }
- func (i *tableCmp) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(i.table.KeyMap)
- }
- func NewLogsTable(app *app.App) TableComponent {
- columns := []table.Column{
- {Title: "ID", Width: 0}, // ID column with zero width
- {Title: "Time", Width: 8},
- {Title: "Level", Width: 7},
- {Title: "Message", Width: 30},
- }
- tableModel := table.New(
- table.WithColumns(columns),
- )
- tableModel.Focus()
- return &tableCmp{
- app: app,
- table: tableModel,
- logs: []logging.Log{},
- }
- }
- // Focus implements the focusable interface
- func (i *tableCmp) Focus() {
- i.focused = true
- i.table.Focus()
- }
- // Blur implements the blurable interface
- func (i *tableCmp) Blur() {
- i.focused = false
- i.table.Blur()
- }
|