summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--internal/llm/agent/agent.go2
-rw-r--r--internal/logging/writer.go15
-rw-r--r--internal/pubsub/broker.go18
-rw-r--r--internal/tui/components/logs/details.go21
-rw-r--r--internal/tui/components/logs/table.go44
-rw-r--r--internal/tui/layout/container.go30
-rw-r--r--internal/tui/page/logs.go160
-rw-r--r--internal/tui/tui.go26
8 files changed, 280 insertions, 36 deletions
diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go
index 662b19701..81b8cf1db 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 50f3367db..4cb89f24f 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 88a59f60a..5aadd8ed5 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 9d7713bbf..7bbfd17dc 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 a6a39c198..fe30c6aa0 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 83aef5879..75c0fe110 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 c3de8684d..1495de8cf 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 62d379c3f..8a8b5f56c 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(),