diff options
Diffstat (limited to 'internal/tui/components/logs')
| -rw-r--r-- | internal/tui/components/logs/details.go | 187 | ||||
| -rw-r--r-- | internal/tui/components/logs/table.go | 207 |
2 files changed, 0 insertions, 394 deletions
diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go deleted file mode 100644 index bc59fdc6f..000000000 --- a/internal/tui/components/logs/details.go +++ /dev/null @@ -1,187 +0,0 @@ -package logs - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/logging" - "github.com/sst/opencode/internal/tui/layout" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" -) - -type DetailComponent interface { - tea.Model - layout.Sizeable - layout.Bindings -} - -type detailCmp struct { - width, height int - currentLog logging.Log - viewport viewport.Model - focused bool -} - -func (i *detailCmp) Init() tea.Cmd { - return nil -} - -func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case selectedLogMsg: - if msg.ID != i.currentLog.ID { - i.currentLog = logging.Log(msg) - // Defer content update to avoid blocking the UI - cmd = tea.Tick(time.Millisecond*1, func(time.Time) tea.Msg { - i.updateContent() - return nil - }) - } - case tea.KeyMsg: - // Only process keyboard input when focused - if !i.focused { - return i, nil - } - // Handle keyboard input for scrolling - i.viewport, cmd = i.viewport.Update(msg) - return i, cmd - } - - return i, cmd -} - -func (i *detailCmp) updateContent() { - var content strings.Builder - t := theme.CurrentTheme() - - // Format the header with timestamp and level - timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted()) - levelStyle := getLevelStyle(i.currentLog.Level) - - // Format timestamp - timeStr := i.currentLog.Timestamp.Format(time.RFC3339) - - header := lipgloss.JoinHorizontal( - lipgloss.Center, - timeStyle.Render(timeStr), - " ", - levelStyle.Render(i.currentLog.Level), - ) - - content.WriteString(lipgloss.NewStyle().Bold(true).Render(header)) - content.WriteString("\n\n") - - // Message with styling - messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) - content.WriteString(messageStyle.Render("Message:")) - content.WriteString("\n") - content.WriteString(lipgloss.NewStyle().Padding(0, 2).Width(i.width).Render(i.currentLog.Message)) - content.WriteString("\n\n") - - // Attributes section - if len(i.currentLog.Attributes) > 0 { - attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) - content.WriteString(attrHeaderStyle.Render("Attributes:")) - content.WriteString("\n") - - // Create a table-like display for attributes - keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true) - valueStyle := lipgloss.NewStyle().Foreground(t.Text()) - - for key, value := range i.currentLog.Attributes { - // if value is JSON, render it with indentation - if strings.HasPrefix(value, "{") { - var indented bytes.Buffer - if err := json.Indent(&indented, []byte(value), "", " "); err != nil { - indented.WriteString(value) - } - value = indented.String() - } - - attrLine := fmt.Sprintf("%s: %s", - keyStyle.Render(key), - valueStyle.Render(value), - ) - - content.WriteString(lipgloss.NewStyle().Padding(0, 2).Width(i.width).Render(attrLine)) - content.WriteString("\n") - } - } - - // Session ID if available - if i.currentLog.SessionID != "" { - sessionStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text()) - content.WriteString("\n") - content.WriteString(sessionStyle.Render("Session:")) - content.WriteString("\n") - content.WriteString(lipgloss.NewStyle().Padding(0, 2).Width(i.width).Render(i.currentLog.SessionID)) - } - - i.viewport.SetContent(content.String()) -} - -func getLevelStyle(level string) lipgloss.Style { - style := lipgloss.NewStyle().Bold(true) - t := theme.CurrentTheme() - - switch strings.ToLower(level) { - case "info": - return style.Foreground(t.Info()) - case "warn", "warning": - return style.Foreground(t.Warning()) - case "error", "err": - return style.Foreground(t.Error()) - case "debug": - return style.Foreground(t.Success()) - default: - return style.Foreground(t.Text()) - } -} - -func (i *detailCmp) View() string { - t := theme.CurrentTheme() - return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background()) -} - -func (i *detailCmp) GetSize() (int, int) { - return i.width, i.height -} - -func (i *detailCmp) SetSize(width int, height int) tea.Cmd { - i.width = width - i.height = height - i.viewport.Width = i.width - i.viewport.Height = i.height - i.updateContent() - return nil -} - -func (i *detailCmp) BindingKeys() []key.Binding { - return layout.KeyMapToSlice(i.viewport.KeyMap) -} - -func NewLogsDetails() DetailComponent { - return &detailCmp{ - viewport: viewport.New(0, 0), - } -} - -// Focus implements the focusable interface -func (i *detailCmp) Focus() { - i.focused = true - i.viewport.SetYOffset(i.viewport.YOffset) -} - -// Blur implements the blurable interface -func (i *detailCmp) Blur() { - i.focused = false -} diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go deleted file mode 100644 index 1fc1daa38..000000000 --- a/internal/tui/components/logs/table.go +++ /dev/null @@ -1,207 +0,0 @@ -package logs - -import ( - // "context" - "fmt" - "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 - // TODO: Logs service not implemented in API yet - logs = []logging.Log{} - err = fmt.Errorf("logs service not implemented") - - 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() -} |
