Browse Source

feat: better logs page

adamdottv 9 months ago
parent
commit
b638dafe5f

+ 1 - 1
internal/llm/agent/agent.go

@@ -307,7 +307,7 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
 	}
 
 	// If we're approaching the context window limit, trigger auto-compaction
-	if (*usage + maxTokens) >= threshold {
+	if false && (*usage+maxTokens) >= threshold {
 		logging.InfoPersist(fmt.Sprintf("Auto-compaction triggered for session %s. Estimated tokens: %d, Threshold: %d", sessionID, usage, threshold))
 
 		// Perform compaction with pause/resume to ensure safety

+ 14 - 1
internal/logging/writer.go

@@ -17,6 +17,11 @@ const (
 	PersistTimeArg = "$_persist_time"
 )
 
+const (
+	// Maximum number of log messages to keep in memory
+	maxLogMessages = 1000
+)
+
 type LogData struct {
 	messages []LogMessage
 	*pubsub.Broker[LogMessage]
@@ -26,7 +31,15 @@ type LogData struct {
 func (l *LogData) Add(msg LogMessage) {
 	l.lock.Lock()
 	defer l.lock.Unlock()
+	
+	// Add new message
 	l.messages = append(l.messages, msg)
+	
+	// Trim if exceeding max capacity
+	if len(l.messages) > maxLogMessages {
+		l.messages = l.messages[len(l.messages)-maxLogMessages:]
+	}
+	
 	l.Publish(pubsub.CreatedEvent, msg)
 }
 
@@ -37,7 +50,7 @@ func (l *LogData) List() []LogMessage {
 }
 
 var defaultLogData = &LogData{
-	messages: make([]LogMessage, 0),
+	messages: make([]LogMessage, 0, maxLogMessages),
 	Broker:   pubsub.NewBroker[LogMessage](),
 }
 

+ 17 - 1
internal/pubsub/broker.go

@@ -5,7 +5,7 @@ import (
 	"sync"
 )
 
-const bufferSize = 64
+const bufferSize = 1000
 
 type Broker[T any] struct {
 	subs      map[chan Event[T]]struct{}
@@ -115,7 +115,23 @@ func (b *Broker[T]) Publish(t EventType, payload T) {
 	for _, sub := range subscribers {
 		select {
 		case sub <- event:
+			// Successfully sent
+		case <-b.done:
+			// Broker is shutting down
+			return
 		default:
+			// Channel is full, but we don't want to block
+			// Log this situation or consider other strategies
+			// For now, we'll create a new goroutine to ensure delivery
+			go func(ch chan Event[T], evt Event[T]) {
+				select {
+				case ch <- evt:
+					// Successfully sent
+				case <-b.done:
+					// Broker is shutting down
+					return
+				}
+			}(sub, event)
 		}
 	}
 }

+ 21 - 0
internal/tui/components/logs/details.go

@@ -25,6 +25,7 @@ type detailCmp struct {
 	width, height int
 	currentLog    logging.LogMessage
 	viewport      viewport.Model
+	focused       bool
 }
 
 func (i *detailCmp) Init() tea.Cmd {
@@ -37,12 +38,21 @@ func (i *detailCmp) Init() tea.Cmd {
 }
 
 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.LogMessage(msg)
 			i.updateContent()
 		}
+	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, nil
@@ -141,3 +151,14 @@ func NewLogsDetails() DetailComponent {
 		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
+}

+ 32 - 12
internal/tui/components/logs/table.go

@@ -21,7 +21,8 @@ type TableComponent interface {
 }
 
 type tableCmp struct {
-	table table.Model
+	table   table.Model
+	focused bool
 }
 
 type selectedLogMsg logging.LogMessage
@@ -38,24 +39,30 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		i.setRows()
 		return i, nil
 	}
-	prevSelectedRow := i.table.SelectedRow()
+	
+	// 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 {
-		if prevSelectedRow == nil || selectedRow[0] == prevSelectedRow[0] {
-			var log logging.LogMessage
-			for _, row := range logging.List() {
-				if row.ID == selectedRow[0] {
-					log = row
-					break
-				}
-			}
-			if log.ID != "" {
-				cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
+		// 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
+				break
 			}
 		}
+		if log.ID != "" {
+			cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
+		}
 	}
 	return i, tea.Batch(cmds...)
 }
@@ -141,3 +148,16 @@ func NewLogsTable() TableComponent {
 		table: tableModel,
 	}
 }
+
+// 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
+	// Table doesn't have a Blur method, but we can implement it here
+	// to satisfy the interface
+}

+ 29 - 1
internal/tui/layout/container.go

@@ -11,6 +11,8 @@ type Container interface {
 	tea.Model
 	Sizeable
 	Bindings
+	Focus() // Add focus method
+	Blur()  // Add blur method
 }
 type container struct {
 	width  int
@@ -29,6 +31,8 @@ type container struct {
 	borderBottom bool
 	borderLeft   bool
 	borderStyle  lipgloss.Border
+	
+	focused bool // Track focus state
 }
 
 func (c *container) Init() tea.Cmd {
@@ -65,7 +69,13 @@ func (c *container) View() string {
 			width--
 		}
 		style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
-		style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
+		
+		// Use primary color for border if focused
+		if c.focused {
+			style = style.BorderBackground(t.Background()).BorderForeground(t.Primary())
+		} else {
+			style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
+		}
 	}
 	style = style.
 		Width(width).
@@ -121,6 +131,24 @@ func (c *container) BindingKeys() []key.Binding {
 	return []key.Binding{}
 }
 
+// Focus sets the container as focused
+func (c *container) Focus() {
+	c.focused = true
+	// Pass focus to content if it supports it
+	if focusable, ok := c.content.(interface{ Focus() }); ok {
+		focusable.Focus()
+	}
+}
+
+// Blur removes focus from the container
+func (c *container) Blur() {
+	c.focused = false
+	// Remove focus from content if it supports it
+	if blurable, ok := c.content.(interface{ Blur() }); ok {
+		blurable.Blur()
+	}
+}
+
 type ContainerOption func(*container)
 
 func NewContainer(content tea.Model, options ...ContainerOption) Container {

+ 143 - 17
internal/tui/page/logs.go

@@ -17,10 +17,40 @@ type LogPage interface {
 	layout.Sizeable
 	layout.Bindings
 }
+
+// Custom keybindings for logs page
+type logsKeyMap struct {
+	Left  key.Binding
+	Right key.Binding
+	Tab   key.Binding
+}
+
+var logsKeys = logsKeyMap{
+	Left: key.NewBinding(
+		key.WithKeys("left", "h"),
+		key.WithHelp("←/h", "left pane"),
+	),
+	Right: key.NewBinding(
+		key.WithKeys("right", "l"),
+		key.WithHelp("→/l", "right pane"),
+	),
+	Tab: key.NewBinding(
+		key.WithKeys("tab"),
+		key.WithHelp("tab", "switch panes"),
+	),
+}
+
 type logsPage struct {
 	width, height int
 	table         layout.Container
 	details       layout.Container
+	activePane    int // 0 = table, 1 = details
+	keyMap        logsKeyMap
+}
+
+// Message to switch active pane
+type switchPaneMsg struct {
+	pane int // 0 = table, 1 = details
 }
 
 func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -30,14 +60,54 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		p.width = msg.Width
 		p.height = msg.Height
 		return p, p.SetSize(msg.Width, msg.Height)
+	case switchPaneMsg:
+		p.activePane = msg.pane
+		if p.activePane == 0 {
+			p.table.Focus()
+			p.details.Blur()
+		} else {
+			p.table.Blur()
+			p.details.Focus()
+		}
+		return p, nil
+	case tea.KeyMsg:
+		// Handle navigation keys
+		switch {
+		case key.Matches(msg, p.keyMap.Left):
+			return p, func() tea.Msg {
+				return switchPaneMsg{pane: 0}
+			}
+		case key.Matches(msg, p.keyMap.Right):
+			return p, func() tea.Msg {
+				return switchPaneMsg{pane: 1}
+			}
+		case key.Matches(msg, p.keyMap.Tab):
+			return p, func() tea.Msg {
+				return switchPaneMsg{pane: (p.activePane + 1) % 2}
+			}
+		}
 	}
 
-	table, cmd := p.table.Update(msg)
-	cmds = append(cmds, cmd)
-	p.table = table.(layout.Container)
-	details, cmd := p.details.Update(msg)
-	cmds = append(cmds, cmd)
-	p.details = details.(layout.Container)
+	// Update the active pane first to handle keyboard input
+	if p.activePane == 0 {
+		table, cmd := p.table.Update(msg)
+		cmds = append(cmds, cmd)
+		p.table = table.(layout.Container)
+
+		// Update details pane without focus
+		details, cmd := p.details.Update(msg)
+		cmds = append(cmds, cmd)
+		p.details = details.(layout.Container)
+	} else {
+		details, cmd := p.details.Update(msg)
+		cmds = append(cmds, cmd)
+		p.details = details.(layout.Container)
+
+		// Update table pane without focus
+		table, cmd := p.table.Update(msg)
+		cmds = append(cmds, cmd)
+		p.table = table.(layout.Container)
+	}
 
 	return p, tea.Batch(cmds...)
 }
@@ -48,14 +118,28 @@ func (p *logsPage) View() string {
 	// Add padding to the right of the table view
 	tableView := lipgloss.NewStyle().PaddingRight(3).Render(p.table.View())
 
+	// Add border to the active pane
+	tableStyle := lipgloss.NewStyle()
+	detailsStyle := lipgloss.NewStyle()
+
+	if p.activePane == 0 {
+		tableStyle = tableStyle.BorderForeground(t.Primary())
+	} else {
+		detailsStyle = detailsStyle.BorderForeground(t.Primary())
+	}
+
+	tableView = tableStyle.Render(tableView)
+	detailsView := detailsStyle.Render(p.details.View())
+
 	return styles.ForceReplaceBackgroundWithLipgloss(
 		lipgloss.JoinVertical(
 			lipgloss.Left,
-			styles.Bold().Render(" esc")+styles.Muted().Render(" to go back"),
+			styles.Bold().Render(" esc")+styles.Muted().Render(" to go back")+
+				"  "+styles.Bold().Render(" tab/←→/h/l")+styles.Muted().Render(" to switch panes"),
 			"",
 			lipgloss.JoinHorizontal(lipgloss.Top,
 				tableView,
-				p.details.View(),
+				detailsView,
 			),
 			"",
 		),
@@ -64,7 +148,21 @@ func (p *logsPage) View() string {
 }
 
 func (p *logsPage) BindingKeys() []key.Binding {
-	return p.table.BindingKeys()
+	// Add our custom keybindings
+	bindings := []key.Binding{
+		p.keyMap.Left,
+		p.keyMap.Right,
+		p.keyMap.Tab,
+	}
+
+	// Add the active pane's keybindings
+	if p.activePane == 0 {
+		bindings = append(bindings, p.table.BindingKeys()...)
+	} else {
+		bindings = append(bindings, p.details.BindingKeys()...)
+	}
+
+	return bindings
 }
 
 // GetSize implements LogPage.
@@ -76,22 +174,50 @@ func (p *logsPage) GetSize() (int, int) {
 func (p *logsPage) SetSize(width int, height int) tea.Cmd {
 	p.width = width
 	p.height = height
+
+	// Account for padding between panes (3 characters)
+	const padding = 3
+	leftPaneWidth := (width - padding) / 2
+	rightPaneWidth := width - leftPaneWidth - padding
+
 	return tea.Batch(
-		p.table.SetSize(width/2, height-3),
-		p.details.SetSize(width/2, height-3),
+		p.table.SetSize(leftPaneWidth, height-3),
+		p.details.SetSize(rightPaneWidth, height-3),
 	)
 }
 
 func (p *logsPage) Init() tea.Cmd {
-	return tea.Batch(
-		p.table.Init(),
-		p.details.Init(),
-	)
+	// Start with table pane active
+	p.activePane = 0
+	p.table.Focus()
+	p.details.Blur()
+
+	// Force an initial selection to update the details pane
+	var cmds []tea.Cmd
+	cmds = append(cmds, p.table.Init())
+	cmds = append(cmds, p.details.Init())
+	
+	// Send a key down and then key up to select the first row
+	// This ensures the details pane is populated when returning to the logs page
+	cmds = append(cmds, func() tea.Msg {
+		return tea.KeyMsg{Type: tea.KeyDown}
+	})
+	cmds = append(cmds, func() tea.Msg {
+		return tea.KeyMsg{Type: tea.KeyUp}
+	})
+	
+	return tea.Batch(cmds...)
 }
 
 func NewLogsPage() LogPage {
+	// Create containers with borders to visually indicate active pane
+	tableContainer := layout.NewContainer(logs.NewLogsTable(), layout.WithBorderHorizontal())
+	detailsContainer := layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderHorizontal())
+
 	return &logsPage{
-		table:   layout.NewContainer(logs.NewLogsTable()),
-		details: layout.NewContainer(logs.NewLogsDetails()),
+		table:      tableContainer,
+		details:    detailsContainer,
+		activePane: 0, // Start with table pane active
+		keyMap:     logsKeys,
 	}
 }

+ 23 - 3
internal/tui/tui.go

@@ -439,7 +439,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					return a, nil
 				}
 				if a.currentPage == page.LogsPage {
-					return a, a.moveToPage(page.ChatPage)
+					// Always allow returning from logs page, even when agent is busy
+					return a, a.moveToPageUnconditional(page.ChatPage)
 				}
 			}
 		case key.Matches(msg, keys.Logs):
@@ -562,8 +563,9 @@ func (a *appModel) RegisterCommand(cmd dialog.Command) {
 }
 
 func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
-	if a.app.CoderAgent.IsBusy() {
-		// For now we don't move to any page if the agent is busy
+	// Allow navigating to logs page even when agent is busy
+	if a.app.CoderAgent.IsBusy() && pageID != page.LogsPage {
+		// Don't move to other pages if the agent is busy
 		return util.ReportWarn("Agent is busy, please wait...")
 	}
 
@@ -583,6 +585,24 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
 	return tea.Batch(cmds...)
 }
 
+// moveToPageUnconditional is like moveToPage but doesn't check if the agent is busy
+func (a *appModel) moveToPageUnconditional(pageID page.PageID) tea.Cmd {
+	var cmds []tea.Cmd
+	if _, ok := a.loadedPages[pageID]; !ok {
+		cmd := a.pages[pageID].Init()
+		cmds = append(cmds, cmd)
+		a.loadedPages[pageID] = true
+	}
+	a.previousPage = a.currentPage
+	a.currentPage = pageID
+	if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
+		cmd := sizable.SetSize(a.width, a.height)
+		cmds = append(cmds, cmd)
+	}
+
+	return tea.Batch(cmds...)
+}
+
 func (a appModel) View() string {
 	components := []string{
 		a.pages[a.currentPage].View(),