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 () => {