mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-06 17:34:58 +01:00
wip: logging improvements
This commit is contained in:
@@ -16,13 +16,13 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/status"
|
||||
"github.com/opencode-ai/opencode/internal/tui/image"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -376,7 +376,7 @@ func (f *filepickerCmp) IsCWDFocused() bool {
|
||||
func NewFilepickerCmp(app *app.App) FilepickerCmp {
|
||||
homepath, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
logging.Error("error loading user files")
|
||||
slog.Error("error loading user files")
|
||||
return nil
|
||||
}
|
||||
baseDir := DirNode{parent: nil, directory: homepath}
|
||||
@@ -392,7 +392,7 @@ func NewFilepickerCmp(app *app.App) FilepickerCmp {
|
||||
|
||||
func (f *filepickerCmp) getCurrentFileBelowCursor() {
|
||||
if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
|
||||
logging.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
|
||||
slog.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
|
||||
f.viewport.SetContent("Preview unavailable")
|
||||
return
|
||||
}
|
||||
@@ -405,7 +405,7 @@ func (f *filepickerCmp) getCurrentFileBelowCursor() {
|
||||
go func() {
|
||||
imageString, err := image.ImagePreview(f.viewport.Width-4, fullPath)
|
||||
if err != nil {
|
||||
logging.Error(err.Error())
|
||||
slog.Error(err.Error())
|
||||
f.viewport.SetContent("Preview unavailable")
|
||||
return
|
||||
}
|
||||
@@ -418,7 +418,7 @@ func (f *filepickerCmp) getCurrentFileBelowCursor() {
|
||||
}
|
||||
|
||||
func readDir(path string, showHidden bool) []os.DirEntry {
|
||||
logging.Info(fmt.Sprintf("Reading directory: %s", path))
|
||||
slog.Info(fmt.Sprintf("Reading directory: %s", path))
|
||||
|
||||
entriesChan := make(chan []os.DirEntry, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
@@ -23,17 +23,12 @@ type DetailComponent interface {
|
||||
|
||||
type detailCmp struct {
|
||||
width, height int
|
||||
currentLog logging.LogMessage
|
||||
currentLog logging.Log
|
||||
viewport viewport.Model
|
||||
focused bool
|
||||
}
|
||||
|
||||
func (i *detailCmp) Init() tea.Cmd {
|
||||
messages := logging.List()
|
||||
if len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
i.currentLog = messages[0]
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,8 +37,12 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case selectedLogMsg:
|
||||
if msg.ID != i.currentLog.ID {
|
||||
i.currentLog = logging.LogMessage(msg)
|
||||
i.updateContent()
|
||||
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
|
||||
@@ -55,7 +54,7 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return i, cmd
|
||||
}
|
||||
|
||||
return i, nil
|
||||
return i, cmd
|
||||
}
|
||||
|
||||
func (i *detailCmp) updateContent() {
|
||||
@@ -66,9 +65,12 @@ func (i *detailCmp) updateContent() {
|
||||
timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
|
||||
levelStyle := getLevelStyle(i.currentLog.Level)
|
||||
|
||||
// Format timestamp
|
||||
timeStr := time.Unix(i.currentLog.Timestamp, 0).Format(time.RFC3339)
|
||||
|
||||
header := lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
|
||||
timeStyle.Render(timeStr),
|
||||
" ",
|
||||
levelStyle.Render(i.currentLog.Level),
|
||||
)
|
||||
@@ -93,23 +95,33 @@ func (i *detailCmp) updateContent() {
|
||||
keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
|
||||
valueStyle := lipgloss.NewStyle().Foreground(t.Text())
|
||||
|
||||
for _, attr := range i.currentLog.Attributes {
|
||||
attrLine := fmt.Sprintf("%s: %s",
|
||||
keyStyle.Render(attr.Key),
|
||||
valueStyle.Render(attr.Value),
|
||||
for key, value := range i.currentLog.Attributes {
|
||||
attrLine := fmt.Sprintf("%s: %s",
|
||||
keyStyle.Render(key),
|
||||
valueStyle.Render(value),
|
||||
)
|
||||
|
||||
content.WriteString(lipgloss.NewStyle().Padding(0, 2).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).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())
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/components/chat"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
// "github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
@@ -23,46 +25,97 @@ type TableComponent interface {
|
||||
type tableCmp struct {
|
||||
table table.Model
|
||||
focused bool
|
||||
logs []logging.Log
|
||||
}
|
||||
|
||||
type selectedLogMsg logging.LogMessage
|
||||
type selectedLogMsg logging.Log
|
||||
|
||||
// Message for when logs are loaded from the database
|
||||
type logsLoadedMsg struct {
|
||||
logs []logging.Log
|
||||
}
|
||||
|
||||
func (i *tableCmp) Init() tea.Cmd {
|
||||
i.setRows()
|
||||
return nil
|
||||
return i.fetchLogs()
|
||||
}
|
||||
|
||||
func (i *tableCmp) fetchLogs() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
loggingService := logging.GetService()
|
||||
if loggingService == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var logs []logging.Log
|
||||
var err error
|
||||
sessionId := session.CurrentSessionID()
|
||||
|
||||
// Limit the number of logs to improve performance
|
||||
const logLimit = 100
|
||||
if sessionId == "" {
|
||||
logs, err = loggingService.ListAll(ctx, logLimit)
|
||||
} else {
|
||||
logs, err = loggingService.ListBySession(ctx, sessionId)
|
||||
// Trim logs if there are too many
|
||||
if err == nil && len(logs) > logLimit {
|
||||
logs = logs[len(logs)-logLimit:]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return logsLoadedMsg{logs: logs}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg.(type) {
|
||||
case pubsub.Event[logging.LogMessage]:
|
||||
i.setRows()
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case logsLoadedMsg:
|
||||
i.logs = msg.logs
|
||||
i.updateRows()
|
||||
return i, nil
|
||||
|
||||
case chat.SessionSelectedMsg:
|
||||
return i, i.fetchLogs()
|
||||
|
||||
case pubsub.Event[logging.Log]:
|
||||
// Only handle created events
|
||||
if msg.Type == pubsub.CreatedEvent {
|
||||
// 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]
|
||||
}
|
||||
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
|
||||
|
||||
// Only send selected log message when selection changes
|
||||
selectedRow := i.table.SelectedRow()
|
||||
if selectedRow != nil {
|
||||
// Always send the selected log message when a row is selected
|
||||
// This fixes the issue where navigation doesn't update the detail pane
|
||||
// when returning to the logs page
|
||||
var log logging.LogMessage
|
||||
for _, row := range logging.List() {
|
||||
if row.ID == selectedRow[0] {
|
||||
log = row
|
||||
// Use a map for faster lookups by ID
|
||||
for _, log := range i.logs {
|
||||
if log.ID == selectedRow[0] {
|
||||
cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
|
||||
break
|
||||
}
|
||||
}
|
||||
if log.ID != "" {
|
||||
cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
|
||||
}
|
||||
}
|
||||
return i, tea.Batch(cmds...)
|
||||
}
|
||||
@@ -105,25 +158,20 @@ func (i *tableCmp) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(i.table.KeyMap)
|
||||
}
|
||||
|
||||
func (i *tableCmp) setRows() {
|
||||
rows := []table.Row{}
|
||||
func (i *tableCmp) updateRows() {
|
||||
rows := make([]table.Row, 0, len(i.logs))
|
||||
|
||||
logs := logging.List()
|
||||
slices.SortFunc(logs, func(a, b logging.LogMessage) int {
|
||||
if a.Time.Before(b.Time) {
|
||||
return 1
|
||||
}
|
||||
if a.Time.After(b.Time) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
// Logs are already sorted by timestamp (newest first) from the database query
|
||||
// Skip the expensive sort operation
|
||||
|
||||
for _, log := range i.logs {
|
||||
// Format timestamp as time
|
||||
timeStr := time.Unix(log.Timestamp, 0).Format("15:04:05")
|
||||
|
||||
for _, log := range logs {
|
||||
// Include ID as hidden first column for selection
|
||||
row := table.Row{
|
||||
log.ID,
|
||||
log.Time.Format("15:04:05"),
|
||||
timeStr,
|
||||
log.Level,
|
||||
log.Message,
|
||||
}
|
||||
@@ -146,6 +194,7 @@ func NewLogsTable() TableComponent {
|
||||
tableModel.Focus()
|
||||
return &tableCmp{
|
||||
table: tableModel,
|
||||
logs: []logging.Log{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
)
|
||||
|
||||
// Manager handles theme registration, selection, and retrieval.
|
||||
@@ -49,19 +49,19 @@ func SetTheme(name string) error {
|
||||
defer globalManager.mu.Unlock()
|
||||
|
||||
delete(styles.Registry, "charm")
|
||||
|
||||
|
||||
// Handle custom theme
|
||||
if name == "custom" {
|
||||
cfg := config.Get()
|
||||
if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
|
||||
return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
|
||||
}
|
||||
|
||||
|
||||
customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load custom theme: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Register the custom theme
|
||||
globalManager.themes["custom"] = customTheme
|
||||
} else if _, exists := globalManager.themes[name]; !exists {
|
||||
@@ -73,7 +73,7 @@ func SetTheme(name string) error {
|
||||
// Update the config file using viper
|
||||
if err := updateConfigTheme(name); err != nil {
|
||||
// Log the error but don't fail the theme change
|
||||
logging.Warn("Warning: Failed to update config file with new theme", "err", err)
|
||||
slog.Warn("Warning: Failed to update config file with new theme", "err", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -140,7 +140,7 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
|
||||
for key, value := range customTheme {
|
||||
adaptiveColor, err := ParseAdaptiveColor(value)
|
||||
if err != nil {
|
||||
logging.Warn("Invalid color definition in custom theme", "key", key, "error", err)
|
||||
slog.Warn("Invalid color definition in custom theme", "key", key, "error", err)
|
||||
continue // Skip this color but continue processing others
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
|
||||
case "diffremovedlinenumberbg":
|
||||
theme.DiffRemovedLineNumberBgColor = adaptiveColor
|
||||
default:
|
||||
logging.Warn("Unknown color key in custom theme", "key", key)
|
||||
slog.Warn("Unknown color key in custom theme", "key", key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
return a, tea.Batch(cmds...)
|
||||
|
||||
case pubsub.Event[logging.LogMessage]:
|
||||
case pubsub.Event[logging.Log]:
|
||||
a.pages[page.LogsPage], cmd = a.pages[page.LogsPage].Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user