Skip to content

Commit c2345cc

Browse files
committed
fix(inquirerer): proper viewport layout with input windowing
- Input area can grow up to (viewportHeight - chrome - 1) lines - Conversation area dynamically shrinks as input grows - Input lines are windowed when they exceed available space - Shows 'ln X/Y' indicator when input is scrolling - Keeps cursor line visible with context when scrolling - Total rendered lines never exceed viewport height
1 parent bbf65a7 commit c2345cc

1 file changed

Lines changed: 48 additions & 22 deletions

File tree

packages/inquirerer/src/ui/aicode.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -746,28 +746,35 @@ export class AICodeUI {
746746
const lines: string[] = [];
747747
const { width } = this.viewport.getTerminalSize();
748748

749+
// Chrome = 3 lines (separator + separator + status bar)
750+
const chromeLines = 3;
751+
// Input can grow up to (viewportHeight - chromeLines - 1) to leave at least 1 line for content
752+
const maxInputLines = Math.max(1, this.viewportHeight - chromeLines - 1);
753+
const actualInputLines = Math.min(this.lineEditor.lines.length, maxInputLines);
754+
755+
// Calculate available space for content (welcome box or conversation)
756+
const availableContentLines = this.viewportHeight - chromeLines - actualInputLines;
757+
749758
// Show welcome box if no messages, otherwise show conversation
750759
if (this.messages.length === 0 && !this.isStreaming) {
751760
const welcomeLines = this.renderWelcomeBox(width);
752-
lines.push(...welcomeLines);
761+
lines.push(...welcomeLines.slice(0, availableContentLines));
753762
} else {
754-
// Render conversation history
755-
const conversationLines = this.renderConversation(width);
763+
// Render conversation history with dynamic max lines
764+
const conversationLines = this.renderConversation(width, availableContentLines);
756765
lines.push(...conversationLines);
757766
}
758767

759-
// Fill remaining space - input area grows freely, conversation shrinks
760-
const inputLineCount = this.lineEditor.lines.length;
761-
const reservedLines = 3 + inputLineCount; // separator + input lines + status
762-
while (lines.length < this.viewportHeight - reservedLines) {
768+
// Fill remaining space to push input to bottom
769+
while (lines.length < availableContentLines) {
763770
lines.push('');
764771
}
765772

766773
// Full-width separator before input
767774
lines.push(dim(BOX.horizontal.repeat(width)));
768775

769-
// Input prompt
770-
const inputLines = this.renderInputLines(width);
776+
// Input prompt (with windowing if needed)
777+
const inputLines = this.renderInputLines(width, actualInputLines);
771778
lines.push(...inputLines);
772779

773780
// Full-width separator after input
@@ -842,9 +849,8 @@ export class AICodeUI {
842849
/**
843850
* Render conversation history
844851
*/
845-
private renderConversation(width: number): string[] {
852+
private renderConversation(width: number, maxLines: number): string[] {
846853
const lines: string[] = [];
847-
const maxLines = this.viewportHeight - 5; // Reserve space for input area
848854

849855
// Collect all message lines
850856
const allLines: string[] = [];
@@ -918,14 +924,31 @@ export class AICodeUI {
918924
}
919925

920926
/**
921-
* Render the input lines (all lines shown, area grows freely)
927+
* Render the input lines (with windowing when lines exceed maxVisibleLines)
922928
*/
923-
private renderInputLines(width: number): string[] {
929+
private renderInputLines(width: number, maxVisibleLines: number): string[] {
924930
const lines: string[] = [];
925931
const inputText = this.getInput();
926932
const hasContent = inputText.length > 0;
933+
const totalLines = this.lineEditor.lines.length;
934+
const cursorLine = this.lineEditor.lineIndex;
935+
936+
// Calculate which lines to show (keep cursor line visible)
937+
let startLine = 0;
938+
if (totalLines > maxVisibleLines) {
939+
// Keep cursor visible with some context
940+
if (cursorLine < Math.floor(maxVisibleLines / 2)) {
941+
startLine = 0;
942+
} else if (cursorLine > totalLines - Math.ceil(maxVisibleLines / 2)) {
943+
startLine = totalLines - maxVisibleLines;
944+
} else {
945+
startLine = cursorLine - Math.floor(maxVisibleLines / 2);
946+
}
947+
}
948+
const endLine = Math.min(startLine + maxVisibleLines, totalLines);
927949

928-
this.lineEditor.lines.forEach((line, idx) => {
950+
for (let idx = startLine; idx < endLine; idx++) {
951+
const line = this.lineEditor.lines[idx];
929952
const prefix = idx === 0 ? cyan('> ') : ' ';
930953
let lineContent: string;
931954

@@ -941,19 +964,22 @@ export class AICodeUI {
941964
lineContent = line;
942965
}
943966

944-
// Add send hint on last line if there's content
945-
if (idx === this.lineEditor.lines.length - 1 && hasContent) {
946-
const hint = dim(' ↵ send');
967+
// Add info on last visible line if there's content
968+
if (idx === endLine - 1 && hasContent) {
969+
// Show line count if scrolling, otherwise show send hint
970+
const info = totalLines > maxVisibleLines
971+
? dim(` ln ${cursorLine + 1}/${totalLines}`)
972+
: dim(' ↵ send');
947973
const lineWidth = displayWidth(prefix + lineContent);
948-
const hintWidth = displayWidth(hint);
949-
if (lineWidth + hintWidth < width - 2) {
950-
const padding = width - lineWidth - hintWidth - 2;
951-
lineContent = lineContent + ' '.repeat(Math.max(0, padding)) + hint;
974+
const infoWidth = displayWidth(info);
975+
if (lineWidth + infoWidth < width - 2) {
976+
const padding = width - lineWidth - infoWidth - 2;
977+
lineContent = lineContent + ' '.repeat(Math.max(0, padding)) + info;
952978
}
953979
}
954980

955981
lines.push(prefix + lineContent);
956-
});
982+
}
957983

958984
return lines;
959985
}

0 commit comments

Comments
 (0)