1package logs
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/charmbracelet/bubbles/key"
9 "github.com/charmbracelet/bubbles/viewport"
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/lipgloss"
12 "github.com/opencode-ai/opencode/internal/logging"
13 "github.com/opencode-ai/opencode/internal/tui/layout"
14 "github.com/opencode-ai/opencode/internal/tui/styles"
15 "github.com/opencode-ai/opencode/internal/tui/theme"
16)
17
18type DetailComponent interface {
19 tea.Model
20 layout.Sizeable
21 layout.Bindings
22}
23
24type detailCmp struct {
25 width, height int
26 currentLog logging.LogMessage
27 viewport viewport.Model
28}
29
30func (i *detailCmp) Init() tea.Cmd {
31 messages := logging.List()
32 if len(messages) == 0 {
33 return nil
34 }
35 i.currentLog = messages[0]
36 return nil
37}
38
39func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
40 switch msg := msg.(type) {
41 case selectedLogMsg:
42 if msg.ID != i.currentLog.ID {
43 i.currentLog = logging.LogMessage(msg)
44 i.updateContent()
45 }
46 }
47
48 return i, nil
49}
50
51func (i *detailCmp) updateContent() {
52 var content strings.Builder
53 t := theme.CurrentTheme()
54
55 // Format the header with timestamp and level
56 timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
57 levelStyle := getLevelStyle(i.currentLog.Level)
58
59 header := lipgloss.JoinHorizontal(
60 lipgloss.Center,
61 timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
62 " ",
63 levelStyle.Render(i.currentLog.Level),
64 )
65
66 content.WriteString(lipgloss.NewStyle().Bold(true).Render(header))
67 content.WriteString("\n\n")
68
69 // Message with styling
70 messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
71 content.WriteString(messageStyle.Render("Message:"))
72 content.WriteString("\n")
73 content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
74 content.WriteString("\n\n")
75
76 // Attributes section
77 if len(i.currentLog.Attributes) > 0 {
78 attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
79 content.WriteString(attrHeaderStyle.Render("Attributes:"))
80 content.WriteString("\n")
81
82 // Create a table-like display for attributes
83 keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
84 valueStyle := lipgloss.NewStyle().Foreground(t.Text())
85
86 for _, attr := range i.currentLog.Attributes {
87 attrLine := fmt.Sprintf("%s: %s",
88 keyStyle.Render(attr.Key),
89 valueStyle.Render(attr.Value),
90 )
91 content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
92 content.WriteString("\n")
93 }
94 }
95
96 i.viewport.SetContent(content.String())
97}
98
99func getLevelStyle(level string) lipgloss.Style {
100 style := lipgloss.NewStyle().Bold(true)
101 t := theme.CurrentTheme()
102
103 switch strings.ToLower(level) {
104 case "info":
105 return style.Foreground(t.Info())
106 case "warn", "warning":
107 return style.Foreground(t.Warning())
108 case "error", "err":
109 return style.Foreground(t.Error())
110 case "debug":
111 return style.Foreground(t.Success())
112 default:
113 return style.Foreground(t.Text())
114 }
115}
116
117func (i *detailCmp) View() string {
118 t := theme.CurrentTheme()
119 return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background())
120}
121
122func (i *detailCmp) GetSize() (int, int) {
123 return i.width, i.height
124}
125
126func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
127 i.width = width
128 i.height = height
129 i.viewport.Width = i.width
130 i.viewport.Height = i.height
131 i.updateContent()
132 return nil
133}
134
135func (i *detailCmp) BindingKeys() []key.Binding {
136 return layout.KeyMapToSlice(i.viewport.KeyMap)
137}
138
139func NewLogsDetails() DetailComponent {
140 return &detailCmp{
141 viewport: viewport.New(0, 0),
142 }
143}