From 9c0bdaf40aa88346fce99fb94497465e268165ba Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 12:34:17 +0000 Subject: [PATCH 1/2] feat: Implement panel focus and fix debug panel scrolling This commit introduces a panel focus system to the TUI. The user can now switch focus between the main panel and the debug panel using the Tab key. Key changes: - Added a `focusedPanel` state to the `AppModel` to track the active panel. - Implemented focus switching with the `Tab` key. - Refactored the `Update` function to delegate input events (key presses, mouse scrolls) only to the focused panel. - Added a visual indicator (yellow border) to the focused panel for better UX. - Adjusted the panel layout to a 2/3 (main) and 1/3 (debug) split. This fixes the issue where the debug panel was not scrollable and was incorrectly consuming input events. --- main.go | 127 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 58 deletions(-) diff --git a/main.go b/main.go index c2783b5..1f420e5 100644 --- a/main.go +++ b/main.go @@ -196,8 +196,16 @@ const ( promptListView ) +type focusedPanel int + +const ( + mainPanelFocus focusedPanel = iota + debugPanelFocus +) + type AppModel struct { state viewState + focusedPanel focusedPanel ctx context.Context session *mcp.ClientSession toolList list.Model @@ -310,6 +318,7 @@ func initialModel(ctx context.Context, session *mcp.ClientSession) *AppModel { return &AppModel{ state: toolSelectionView, + focusedPanel: mainPanelFocus, ctx: ctx, session: session, toolList: toolList, @@ -361,9 +370,17 @@ func (m AppModel) Init() tea.Cmd { func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd - var cmds []tea.Cmd switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + debugPanelWidth := m.width / 3 + m.debugViewport.Width = debugPanelWidth - 2 + m.debugViewport.Height = m.height - 2 + m.debugViewport, cmd = m.debugViewport.Update(msg) + return m, cmd + case toolResult: if msg.err != nil { m.err = msg.err @@ -374,6 +391,8 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.logf("Result:\n========\n%s", msg.result) m.result = msg.result + return m, nil + case resourceResult: if msg.err != nil { m.err = msg.err @@ -384,11 +403,21 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.logf("Result:\n========\n%s", msg.result) m.resourceResult = msg.result + return m, nil + case tea.KeyMsg: if verbose { m.logf("Key pressed: %s", msg.String()) } + // Global key bindings that work regardless of focus switch msg.Type { + case tea.KeyTab: + if m.focusedPanel == mainPanelFocus { + m.focusedPanel = debugPanelFocus + } else { + m.focusedPanel = mainPanelFocus + } + return m, nil case tea.KeyEsc: if m.state == resourceDetailView { m.state = resourceListView @@ -399,54 +428,29 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyCtrlC: return m, tea.Quit } + } - switch m.state { - case toolSelectionView: - var model tea.Model - model, cmd = m.updateToolSelectionView(msg) - cmds = append(cmds, cmd) - m.debugViewport, cmd = m.debugViewport.Update(msg) - cmds = append(cmds, cmd) - return model, tea.Batch(cmds...) - case resourceListView: - var model tea.Model - model, cmd = m.updateResourceListView(msg) - cmds = append(cmds, cmd) - m.debugViewport, cmd = m.debugViewport.Update(msg) - cmds = append(cmds, cmd) - return model, tea.Batch(cmds...) - case promptListView: - var model tea.Model - model, cmd = m.updatePromptListView(msg) - cmds = append(cmds, cmd) - m.debugViewport, cmd = m.debugViewport.Update(msg) - cmds = append(cmds, cmd) - return model, tea.Batch(cmds...) - case argumentInputView: - var model tea.Model - model, cmd = m.updateArgumentInputView(msg) - cmds = append(cmds, cmd) - m.debugViewport, cmd = m.debugViewport.Update(msg) - cmds = append(cmds, cmd) - return model, tea.Batch(cmds...) - case resourceDetailView: - m.debugViewport, cmd = m.debugViewport.Update(msg) - cmds = append(cmds, cmd) - return m, tea.Batch(cmds...) - } - - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - debugWidth := m.width / 3 - m.debugViewport.Width = debugWidth - m.debugViewport.Height = m.height - 2 + // Delegate message to the focused panel + if m.focusedPanel == debugPanelFocus { + m.debugViewport, cmd = m.debugViewport.Update(msg) + return m, cmd } - m.debugViewport, cmd = m.debugViewport.Update(msg) - cmds = append(cmds, cmd) + // Main panel has focus, delegate to the active view + switch m.state { + case toolSelectionView: + return m.updateToolSelectionView(msg) + case resourceListView: + return m.updateResourceListView(msg) + case promptListView: + return m.updatePromptListView(msg) + case argumentInputView: + return m.updateArgumentInputView(msg) + case resourceDetailView: + return m, nil + } - return m, tea.Batch(cmds...) + return m, nil } func (m *AppModel) updateToolSelectionView(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -586,19 +590,19 @@ func (m AppModel) View() string { return fmt.Sprintf("Error: %v\n\nPress ctrl+c to quit.", m.err) } - mainWidth := m.width / 3 - //debugWidth := m.width - mainWidth + debugPanelWidth := m.width / 3 + mainPanelWidth := m.width - debugPanelWidth var mainContent strings.Builder switch m.state { case toolSelectionView: - m.toolList.SetSize(mainWidth-2, m.height-2) + m.toolList.SetSize(mainPanelWidth-2, m.height-2) mainContent.WriteString(m.toolList.View()) case resourceListView: - m.resourceList.SetSize(mainWidth-2, m.height-2) + m.resourceList.SetSize(mainPanelWidth-2, m.height-2) mainContent.WriteString(m.resourceList.View()) case promptListView: - m.promptList.SetSize(mainWidth-2, m.height-2) + m.promptList.SetSize(mainPanelWidth-2, m.height-2) mainContent.WriteString(m.promptList.View()) case resourceDetailView: var b strings.Builder @@ -619,17 +623,24 @@ func (m AppModel) View() string { mainContent.WriteString(b.String()) } - mainPanel := lipgloss.NewStyle(). + mainPanelStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). - Width(mainWidth). - Height(m.height - 2). - Render(mainContent.String()) + Width(mainPanelWidth - 2). + Height(m.height - 2) - debugPanel := lipgloss.NewStyle(). + debugPanelStyle := lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). - Width(m.debugViewport.Width). - Height(m.debugViewport.Height). - Render(m.debugViewport.View()) + Width(debugPanelWidth - 2). + Height(m.debugViewport.Height) + + if m.focusedPanel == mainPanelFocus { + mainPanelStyle = mainPanelStyle.BorderForeground(lipgloss.Color("228")) // Yellow + } else { + debugPanelStyle = debugPanelStyle.BorderForeground(lipgloss.Color("228")) // Yellow + } + + mainPanel := mainPanelStyle.Render(mainContent.String()) + debugPanel := debugPanelStyle.Render(m.debugViewport.View()) return lipgloss.JoinHorizontal(lipgloss.Top, mainPanel, debugPanel) } From c1c1bf3c423c4ba0a321158c9f4a81779ac78aca Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 15:40:05 +0000 Subject: [PATCH 2/2] fix: Fix debug panel scrolling and resource logging This commit addresses follow-up issues from the initial panel focus implementation. - Enables mouse events in the Bubble Tea program by adding `tea.WithMouseCellMotion()`. This was the root cause of the debug panel not being scrollable via mouse. - Removes redundant logging of full resource details to the debug panel. The details are already shown in the main view, and this change reduces clutter in the logs. --- main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/main.go b/main.go index 1f420e5..6c91776 100644 --- a/main.go +++ b/main.go @@ -401,7 +401,6 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if verbose { m.logf("Resource result received") } - m.logf("Result:\n========\n%s", msg.result) m.resourceResult = msg.result return m, nil @@ -788,7 +787,7 @@ func handleSession(ctx context.Context, session *mcp.ClientSession) { } defer f.Close() } - p := tea.NewProgram(initialModel(ctx, session), tea.WithAltScreen()) + p := tea.NewProgram(initialModel(ctx, session), tea.WithAltScreen(), tea.WithMouseCellMotion()) if _, err := p.Run(); err != nil { log.Fatalf("Error running program: %v", err) }