Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/tui/keybinding_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
52 changes: 52 additions & 0 deletions internal/tui/keybinding_help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
18 changes: 13 additions & 5 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -2194,13 +2195,20 @@ 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)
mcpOverlay := m.mcpManagerOverlay(width)
pickerOverlay := m.pickerOverlay(width)
viewportOverlay := ""
switch {
case helpOverlayContent != "":
viewportOverlay = helpOverlayContent
case providerOverlay != "":
viewportOverlay = providerOverlay
case mcpAddOverlay != "":
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/sidebar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading