1package logs
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/viewport"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/lipgloss/v2"
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/util"
16)
17
18type DetailComponent interface {
19 util.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 := styles.CurrentTheme()
54
55 // Format the header with timestamp and level
56 timeStyle := t.S().Muted
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 := t.S().Text.Bold(true)
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 := t.S().Text.Bold(true)
79 content.WriteString(attrHeaderStyle.Render("Attributes:"))
80 content.WriteString("\n")
81
82 // Create a table-like display for attributes
83 keyStyle := t.S().Base.Foreground(t.Primary).Bold(true)
84 valueStyle := t.S().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 t := styles.CurrentTheme()
101 style := t.S().Base.Bold(true)
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.FgBase)
114 }
115}
116
117func (i *detailCmp) View() tea.View {
118 return tea.NewView(i.viewport.View())
119}
120
121func (i *detailCmp) GetSize() (int, int) {
122 return i.width, i.height
123}
124
125func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
126 i.width = width
127 i.height = height
128 i.viewport.SetWidth(i.width)
129 i.viewport.SetHeight(i.height)
130 i.updateContent()
131 return nil
132}
133
134func (i *detailCmp) BindingKeys() []key.Binding {
135 return layout.KeyMapToSlice(i.viewport.KeyMap)
136}
137
138func NewLogsDetails() DetailComponent {
139 return &detailCmp{
140 viewport: viewport.New(),
141 }
142}