mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix: limit selector window height (#11072)
With this PR, we stop rendering every option. Instead, we render a centered, clamped window of up to 10 options and show `(selection/total)` when the list overflows. This keeps the selected option visible in long extension-provided lists without changing navigation, keybindings, callbacks, countdowns, or short-list rendering. Fixes #6688. ## Merge Danger **Door:** Two-way The change is localized to `ExtensionSelectorComponent.updateList()` and can be reverted without migration or persisted-state concerns. **Blast Radius:** Low Only extension selectors with more than 10 options change appearance; they now render a window and position indicator instead of every option. --------- Co-authored-by: Pi via openai-codex (gpt-5.6-sol) <noreply@openai-codex>
This commit is contained in:
parent
f6278a472e
commit
3b57ed7e27
2 changed files with 97 additions and 1 deletions
|
|
@ -84,13 +84,27 @@ export class ExtensionSelectorComponent extends Container {
|
|||
|
||||
private updateList(): void {
|
||||
this.listContainer.clear();
|
||||
for (let i = 0; i < this.options.length; i++) {
|
||||
|
||||
const maxVisible = 10;
|
||||
// Center the window on the selection, then clamp it to the list boundaries.
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.options.length - maxVisible),
|
||||
);
|
||||
const endIndex = Math.min(startIndex + maxVisible, this.options.length);
|
||||
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
const isSelected = i === this.selectedIndex;
|
||||
const text = isSelected
|
||||
? theme.fg("accent", "→ ") + theme.fg("accent", this.options[i])
|
||||
: ` ${theme.fg("text", this.options[i])}`;
|
||||
this.listContainer.addChild(new Text(text, 1, 0));
|
||||
}
|
||||
|
||||
if (startIndex > 0 || endIndex < this.options.length) {
|
||||
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.options.length})`);
|
||||
this.listContainer.addChild(new Text(scrollInfo, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
|
|
|
|||
82
packages/coding-agent/test/extension-selector.test.ts
Normal file
82
packages/coding-agent/test/extension-selector.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { setKeybindings } from "@earendil-works/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.ts";
|
||||
import { ExtensionSelectorComponent } from "../src/modes/interactive/components/extension-selector.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
import { stripAnsi } from "../src/utils/ansi.ts";
|
||||
|
||||
function render(selector: ExtensionSelectorComponent): string {
|
||||
return stripAnsi(selector.render(80).join("\n"));
|
||||
}
|
||||
|
||||
function visibleOptions(output: string): string[] {
|
||||
return output
|
||||
.split("\n")
|
||||
.filter((line) => line.includes("option-"))
|
||||
.map((line) => line.trim().replace(/^→ /, ""));
|
||||
}
|
||||
|
||||
describe("extension selector", () => {
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
// #6688: long extension selectors must keep the selected option visible.
|
||||
it("windows long option lists around the selection", () => {
|
||||
const options = Array.from({ length: 15 }, (_, index) => `option-${index + 1}`);
|
||||
const selector = new ExtensionSelectorComponent(
|
||||
"Choose",
|
||||
options,
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
let output = render(selector);
|
||||
expect(visibleOptions(output)).toEqual(options.slice(0, 10));
|
||||
expect(output).toContain("→ option-1");
|
||||
expect(output).toContain(" (1/15)");
|
||||
|
||||
for (let i = 0; i < 7; i++) selector.handleInput("\x1b[B");
|
||||
output = render(selector);
|
||||
expect(visibleOptions(output)).toEqual(options.slice(2, 12));
|
||||
expect(output).toContain("→ option-8");
|
||||
expect(output).toContain(" (8/15)");
|
||||
|
||||
for (let i = 0; i < 7; i++) selector.handleInput("\x1b[B");
|
||||
output = render(selector);
|
||||
expect(visibleOptions(output)).toEqual(options.slice(5, 15));
|
||||
expect(output).toContain("→ option-15");
|
||||
expect(output).toContain(" (15/15)");
|
||||
});
|
||||
|
||||
it("renders short and empty option lists without a scroll indicator", () => {
|
||||
const shortOptions = ["option-1", "option-2", "option-3"];
|
||||
const shortOutput = render(
|
||||
new ExtensionSelectorComponent(
|
||||
"Choose",
|
||||
shortOptions,
|
||||
() => {},
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
expect(visibleOptions(shortOutput)).toEqual(shortOptions);
|
||||
expect(shortOutput).toContain("→ option-1");
|
||||
expect(shortOutput).not.toContain("(1/3)");
|
||||
|
||||
const emptyOutput = render(
|
||||
new ExtensionSelectorComponent(
|
||||
"Choose",
|
||||
[],
|
||||
() => {},
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
expect(emptyOutput).toContain("Choose");
|
||||
expect(emptyOutput).toContain("↑↓ navigate");
|
||||
expect(emptyOutput).not.toContain("→");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue