diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 662b1970..81b8cf1d 100644 --- a/internal/llm/agent/agent.go +++ b/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 diff --git a/internal/logging/writer.go b/internal/logging/writer.go index 50f3367d..4cb89f24 100644 --- a/internal/logging/writer.go +++ b/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](), } diff --git a/internal/pubsub/broker.go b/internal/pubsub/broker.go index 88a59f60..5aadd8ed 100644 --- a/internal/pubsub/broker.go +++ b/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) } } } diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go index 9d7713bb..7bbfd17d 100644 --- a/internal/tui/components/logs/details.go +++ b/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 +} diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go index a6a39c19..fe30c6aa 100644 --- a/internal/tui/components/logs/table.go +++ b/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 +} diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go index 83aef587..75c0fe11 100644 --- a/internal/tui/layout/container.go +++ b/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 { diff --git a/internal/tui/page/logs.go b/internal/tui/page/logs.go index c3de8684..1495de8c 100644 --- a/internal/tui/page/logs.go +++ b/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, } } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 62d379c3..8a8b5f56 100644 --- a/internal/tui/tui.go +++ b/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(),