From 76a96fe256499445079a45781238fda22986aea1 Mon Sep 17 00:00:00 2001 From: ferologics Date: Wed, 25 Mar 2026 23:19:51 +0100 Subject: [PATCH] fix keyboard help overlay overlap --- src/ui/components/chrome/HelpDialog.tsx | 52 ++++++++++++++++++------- test/ui-components.test.tsx | 49 ++++++++++++++++++----- 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/ui/components/chrome/HelpDialog.tsx b/src/ui/components/chrome/HelpDialog.tsx index 6b9ebd7e..11d793c8 100644 --- a/src/ui/components/chrome/HelpDialog.tsx +++ b/src/ui/components/chrome/HelpDialog.tsx @@ -53,14 +53,40 @@ export function HelpDialog({ const bodyWidth = Math.max(1, width - 4); const keyWidth = Math.min(16, Math.max(12, Math.floor(bodyWidth * 0.28))); const descriptionWidth = Math.max(1, bodyWidth - keyWidth); - const height = Math.min( - sections.reduce((total, section) => total + 1 + section.items.length, 0) + 3, - Math.max(8, terminalHeight - 2), + const sectionSpacerRowCount = Math.max(0, sections.length - 1); + const contentRowCount = + sections.reduce((rowCount, section) => rowCount + 1 + section.items.length, 0) + + sectionSpacerRowCount; + // ModalFrame contributes the border rows, title row, padding, and one blank spacer row. + const modalFrameChromeRowCount = 6; + const requiredModalHeight = contentRowCount + modalFrameChromeRowCount; + const modalHeight = Math.min(requiredModalHeight, Math.max(8, terminalHeight - 2)); + const shouldScroll = modalHeight < requiredModalHeight; + const content = ( + + {sections.map((section, sectionIndex) => ( + + + {section.title} + + {section.items.map(([keys, description]) => ( + + {padText(fitText(keys, keyWidth), keyWidth)} + {fitText(description, descriptionWidth)} + + ))} + {sectionIndex < sections.length - 1 ? : null} + + ))} + ); return ( - {sections.map((section) => ( - - {section.title} - {section.items.map(([keys, description]) => ( - - {padText(fitText(keys, keyWidth), keyWidth)} - {fitText(description, descriptionWidth)} - - ))} - - ))} + {shouldScroll ? ( + + {content} + + ) : ( + content + )} ); } diff --git a/test/ui-components.test.tsx b/test/ui-components.test.tsx index 588f93b1..9476db46 100644 --- a/test/ui-components.test.tsx +++ b/test/ui-components.test.tsx @@ -836,27 +836,56 @@ describe("UI components", () => { expect(frame).toContain("beta"); }); - test("HelpDialog renders the grouped keyboard help modal", async () => { + test("HelpDialog renders every keyboard shortcut row without overlap", async () => { const theme = resolveTheme("midnight", null); const frame = await captureFrame( {}} />, 76, - 20, + 28, ); - expect(frame).toContain("Keyboard help"); - expect(frame).toContain("[Esc]"); - expect(frame).toContain("Navigation"); - expect(frame).toContain("View"); - expect(frame).toContain("Review"); - expect(frame).toContain("F10"); - expect(frame).toContain("reload / quit"); + const expectedRows = [ + "Keyboard help", + "[Esc]", + "Navigation", + "↑ / ↓ move line-by-line", + "Space / f page down (alt: f)", + "b page up", + "Shift+Space page up (alt)", + "d / u half page down / up", + "[ / ] previous / next hunk", + "Home / End jump to top / bottom", + "View", + "1 / 2 / 0 split / stack / auto", + "s / t sidebar / theme", + "a toggle AI notes", + "l / w / m lines / wrap / metadata", + "Review", + "/ focus file filter", + "Tab swap files / filter focus", + "F10 open menus", + "r / q reload / quit", + ] as const; + + for (const expectedRow of expectedRows) { + expect(frame).toContain(expectedRow); + } + + const lines = frame.split("\n"); + const blankModalRow = /│\s+│/; + const viewHeaderIndex = lines.findIndex((line) => line.includes("│ View")); + const reviewHeaderIndex = lines.findIndex((line) => line.includes("│ Review")); + + expect(lines[viewHeaderIndex - 1]).toMatch(blankModalRow); + expect(lines[reviewHeaderIndex - 1]).toMatch(blankModalRow); + expect(frame).not.toContain("linese/Awrapt/smetadata"); + expect(frame).not.toContain("reloade/uquit"); }); test("DiffSectionPlaceholder preserves offscreen section chrome without mounting rows", async () => {