openapi
This commit is contained in:
208
pkg/tui/components/logs/table.go
Normal file
208
pkg/tui/components/logs/table.go
Normal file
@@ -0,0 +1,208 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user