From e4cda45af0c8ce3d81b4a761cd9ec14782c82b04 Mon Sep 17 00:00:00 2001 From: N1xev Date: Fri, 3 Jul 2026 04:09:00 +0300 Subject: [PATCH 1/4] fix(tui): compose help overlay through the viewport overlay pipeline exactly like how other popups works. --- internal/tui/keybinding_help.go | 6 +++--- internal/tui/model.go | 18 +++++++++++++----- internal/tui/sidebar.go | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/internal/tui/keybinding_help.go b/internal/tui/keybinding_help.go index f12d6a13e..0e2ca0e3d 100644 --- a/internal/tui/keybinding_help.go +++ b/internal/tui/keybinding_help.go @@ -124,9 +124,9 @@ func formatKeybindingLine(binding keybinding, keyColumn int, innerWidth int) str return " " + keyCell + " " + desc } -// renderKeybindingHelpOverlay renders the framed, centered `?` help overlay for -// the given terminal dimensions. -func (m model) renderKeybindingHelpOverlay(width int, height int) string { +// the given terminal width. Vertical centering is handled by the caller's +// overlay compositing pipeline (overlayViewportLines in transcriptView). +func (m model) renderKeybindingHelpOverlay(width int) string { overlayWidth := keybindingHelpOverlayWidth(width) lines := m.renderKeybindingHelpLines(overlayWidth - 4) block := styledBlockFillTitle(overlayWidth, "Keyboard Shortcuts", lines, zeroTheme.line, zeroTheme.panel) diff --git a/internal/tui/model.go b/internal/tui/model.go index 964fe9b43..569f16afc 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -2118,12 +2118,13 @@ func (m model) View() tea.View { var content string if m.setup.visible { content = m.setupView(chatWidth(m.width)) - } else if m.helpOverlay { - content = m.renderKeybindingHelpOverlay(chatWidth(m.width), m.height) - } else if m.transcriptDetailed { - content = m.detailedTranscriptView() - } else { + } else if m.helpOverlay || !m.transcriptDetailed { + // When helpOverlay is active the help panel is composited into the normal + // transcript view as a true overlay (scrim + vertical centering), matching + // how the suggestion picker / provider wizard / pickers are drawn. content = m.transcriptView() + } else { + content = m.detailedTranscriptView() } view := tea.NewView(content) @@ -2194,6 +2195,11 @@ func (m model) transcriptView() string { return body + footer } + helpOverlayContent := "" + if m.helpOverlay { + helpOverlayContent = m.renderKeybindingHelpOverlay(width) + } + suggestionOverlay := m.suggestionOverlay(width) providerOverlay := m.providerWizardOverlay(width) mcpAddOverlay := m.mcpAddWizardOverlay(width) @@ -2201,6 +2207,8 @@ func (m model) transcriptView() string { pickerOverlay := m.pickerOverlay(width) viewportOverlay := "" switch { + case helpOverlayContent != "": + viewportOverlay = helpOverlayContent case providerOverlay != "": viewportOverlay = providerOverlay case mcpAddOverlay != "": diff --git a/internal/tui/sidebar.go b/internal/tui/sidebar.go index f43d014f3..32753afcd 100644 --- a/internal/tui/sidebar.go +++ b/internal/tui/sidebar.go @@ -70,7 +70,7 @@ func (m model) sidebarAvailable() bool { // list) take over the chat column and render at full width; suppress the // second column while any is active so their geometry and mouse hit-testing // stay full-width as before. - if m.setup.visible || m.providerWizard != nil || m.mcpAddWizard != nil || + if m.setup.visible || m.helpOverlay || m.providerWizard != nil || m.mcpAddWizard != nil || m.mcpManager != nil || m.picker != nil || m.suggestionsActive() { return false } From 22708a4cd6dc167264a40283fb1e3e22d2110c4c Mon Sep 17 00:00:00 2001 From: N1xev Date: Fri, 3 Jul 2026 04:48:52 +0300 Subject: [PATCH 2/4] fix for the CI/CD of formatting --- internal/tui/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/model.go b/internal/tui/model.go index 569f16afc..9cd5d84cd 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -2199,7 +2199,7 @@ func (m model) transcriptView() string { if m.helpOverlay { helpOverlayContent = m.renderKeybindingHelpOverlay(width) } - + suggestionOverlay := m.suggestionOverlay(width) providerOverlay := m.providerWizardOverlay(width) mcpAddOverlay := m.mcpAddWizardOverlay(width) From 6ee19c5d81a7194787ec2e7abbb4d049e820d96c Mon Sep 17 00:00:00 2001 From: N1xev Date: Fri, 3 Jul 2026 05:03:14 +0300 Subject: [PATCH 3/4] Added regression tests for the help overlay --- internal/tui/keybinding_help_test.go | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/internal/tui/keybinding_help_test.go b/internal/tui/keybinding_help_test.go index c7d78671f..43d621400 100644 --- a/internal/tui/keybinding_help_test.go +++ b/internal/tui/keybinding_help_test.go @@ -105,3 +105,55 @@ func TestKeybindingGroupsAreWellFormed(t *testing.T) { } } } + +// #419: the `?` help overlay must render ON TOP of the chat (like the model +// picker), not REPLACE the whole screen. The old full-screen replace produced +// only the centered shortcut block on a blank canvas — no title bar, no +// composer. With the overlay open, surrounding chat chrome that is NOT covered +// by the centered box (the model title bar at top, the composer at bottom) must +// still be present alongside "Keyboard Shortcuts". +func TestHelpOverlayCompositesOverChatNotReplacingIt(t *testing.T) { + m := newModel(context.Background(), Options{ModelName: "gpt-4o"}) + m.width = 100 + m.height = 40 + m.altScreen = true + + base := plainRender(t, m.View()) // no overlay: baseline chrome + m.helpOverlay = true + over := plainRender(t, m.View()) + + if !strings.Contains(over, "Keyboard Shortcuts") { + t.Fatalf("help overlay not rendered:\n%s", over) + } + // Chrome that renders in the baseline (and sits outside the centered overlay + // box) must survive behind the overlay. The full-screen replace showed none. + for _, marker := range []string{"gpt-4o", "describe a task"} { + if !strings.Contains(base, marker) { + t.Fatalf("precondition: baseline chat should contain %q:\n%s", marker, base) + } + if !strings.Contains(over, marker) { + t.Fatalf("#419: help replaced the chat instead of overlaying it; %q is gone:\n%s", marker, over) + } + } +} + +// A populated transcript row also survives behind the overlay (peeking out to +// the left of the centered box), proving the chat body — not just the chrome — +// is composited under the overlay rather than discarded. +func TestHelpOverlayKeepsTranscriptBodyBehindIt(t *testing.T) { + m := newModel(context.Background(), Options{ModelName: "gpt-4o"}) + m.width = 120 + m.height = 40 + m.altScreen = true + m.transcript = appendTranscriptRow(m.transcript, transcriptRow{kind: rowUser, text: "hello there this is a chat line"}) + m.helpOverlay = true + + view := plainRender(t, m.View()) + if !strings.Contains(view, "Keyboard Shortcuts") { + t.Fatalf("help overlay not rendered:\n%s", view) + } + // The start of the transcript line peeks to the left of the centered box. + if !strings.Contains(view, "hello") { + t.Fatalf("#419: transcript body was replaced by the help overlay:\n%s", view) + } +} \ No newline at end of file From ede6b31dff73699aacaec8d79cc581a0d961620b Mon Sep 17 00:00:00 2001 From: N1xev Date: Fri, 3 Jul 2026 05:54:59 +0300 Subject: [PATCH 4/4] formatted code --- internal/tui/keybinding_help_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/keybinding_help_test.go b/internal/tui/keybinding_help_test.go index 43d621400..7619c2168 100644 --- a/internal/tui/keybinding_help_test.go +++ b/internal/tui/keybinding_help_test.go @@ -156,4 +156,4 @@ func TestHelpOverlayKeepsTranscriptBodyBehindIt(t *testing.T) { if !strings.Contains(view, "hello") { t.Fatalf("#419: transcript body was replaced by the help overlay:\n%s", view) } -} \ No newline at end of file +}