fix(tui): scroll the /model Roles view so clipped rows stay reachable
The Roles panel renderer looped from the first row with a hard height cap and no scroll offset, so roles and model-keyed fallback chains past the visible height were never drawn and unreachable by keyboard, with no truncation indicator. Unlike the sibling provider list (model-browser windows via #windowStart/#ensureSelectedVisible), the Roles panel had no equivalent. Window #renderRolesView around #roleIndex via #ensureRoleVisible, offset mouse hit-testing by the scroll start bounded to the visible count, and draw an up/down '+N more' hint when the list is clipped. Fixes #8817
This commit is contained in:
@@ -207,6 +207,10 @@ export class ModelHubComponent implements Component {
|
||||
#rolesRows: RolesRow[] = [];
|
||||
#roleIndex = 0;
|
||||
#roleHover: number | null = null;
|
||||
/** First roles row drawn in the scroll window; follows the cursor and clamps to the list. */
|
||||
#roleScrollStart = 0;
|
||||
/** Roles rows actually drawn this frame; bounds mouse hit-testing to the visible window. */
|
||||
#rolesVisibleCount = 0;
|
||||
|
||||
#assigning: AssignTarget | null = null;
|
||||
#strip: StripState | null = null;
|
||||
@@ -1302,6 +1306,15 @@ export class ModelHubComponent implements Component {
|
||||
}
|
||||
}
|
||||
|
||||
/** Scroll `#roleScrollStart` just enough to keep `#roleIndex` inside a window of `viewHeight` rows, clamped to the list. */
|
||||
#ensureRoleVisible(viewHeight: number, total: number): number {
|
||||
if (viewHeight <= 0) return 0;
|
||||
let start = this.#roleScrollStart;
|
||||
if (this.#roleIndex < start) start = this.#roleIndex;
|
||||
else if (this.#roleIndex >= start + viewHeight) start = this.#roleIndex - viewHeight + 1;
|
||||
return Math.max(0, Math.min(start, Math.max(0, total - viewHeight)));
|
||||
}
|
||||
|
||||
/** Step the roles cursor by one row, skipping separator rows. Wraps at the ends unless `wrap: false` (then the cursor stays put). */
|
||||
#stepRoleIndex(from: number, delta: -1 | 1, options: { wrap?: boolean } = {}): number {
|
||||
const wrap = options.wrap ?? true;
|
||||
@@ -1473,7 +1486,8 @@ export class ModelHubComponent implements Component {
|
||||
this.#sidebarHover = overSidebar ? this.#sidebarEntryIndexAt(contentLine) : null;
|
||||
if (overBody && entry.kind === "roles" && this.#assigning === null) {
|
||||
const roleLine = bodyLine - this.#rolesRowStart;
|
||||
this.#roleHover = roleLine >= 0 && roleLine < this.#rolesRowCount ? roleLine : null;
|
||||
this.#roleHover =
|
||||
roleLine >= 0 && roleLine < this.#rolesVisibleCount ? roleLine + this.#roleScrollStart : null;
|
||||
} else {
|
||||
this.#roleHover = null;
|
||||
if (overBody && this.#isBrowserView(entry)) {
|
||||
@@ -1508,8 +1522,9 @@ export class ModelHubComponent implements Component {
|
||||
if (overBody) {
|
||||
if (entry.kind === "roles" && this.#assigning === null) {
|
||||
this.#focus = "list";
|
||||
const roleLine = bodyLine - this.#rolesRowStart;
|
||||
if (roleLine >= 0 && roleLine < this.#rolesRowCount) {
|
||||
const listLine = bodyLine - this.#rolesRowStart;
|
||||
if (listLine >= 0 && listLine < this.#rolesVisibleCount) {
|
||||
const roleLine = listLine + this.#roleScrollStart;
|
||||
const rowDef = this.#rolesRows[roleLine];
|
||||
if (rowDef && rowDef.kind !== "separator") {
|
||||
if (roleLine === this.#roleIndex) {
|
||||
@@ -1718,7 +1733,16 @@ export class ModelHubComponent implements Component {
|
||||
|
||||
const cycleOrder = this.#cycleOrder();
|
||||
const listFocused = this.#focus === "list";
|
||||
for (let i = 0; i < this.#rolesRows.length && lines.length < rows - 2; i++) {
|
||||
// Window the list around the cursor so entries past the panel height stay
|
||||
// reachable; the trailing indicator line steals one row when clipped.
|
||||
const total = this.#rolesRows.length;
|
||||
const capacity = Math.max(0, rows - 2 - this.#rolesRowStart);
|
||||
const overflow = total > capacity;
|
||||
const viewHeight = overflow ? Math.max(0, capacity - 1) : capacity;
|
||||
this.#roleScrollStart = this.#ensureRoleVisible(viewHeight, total);
|
||||
const endIndex = Math.min(this.#roleScrollStart + viewHeight, total);
|
||||
this.#rolesVisibleCount = Math.max(0, endIndex - this.#roleScrollStart);
|
||||
for (let i = this.#roleScrollStart; i < endIndex; i++) {
|
||||
const rowDef = this.#rolesRows[i];
|
||||
if (!rowDef) continue;
|
||||
const selected = i === this.#roleIndex;
|
||||
@@ -1802,6 +1826,15 @@ export class ModelHubComponent implements Component {
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
if (overflow) {
|
||||
const hiddenAbove = this.#roleScrollStart;
|
||||
const hiddenBelow = total - endIndex;
|
||||
const parts: string[] = [];
|
||||
if (hiddenAbove > 0) parts.push(`↑ ${hiddenAbove} more`);
|
||||
if (hiddenBelow > 0) parts.push(`↓ ${hiddenBelow} more`);
|
||||
lines.push(truncateToWidth(theme.fg("dim", ` ${parts.join(" ")}`), width));
|
||||
}
|
||||
|
||||
// Live preview of the quick-switch cycle, rendered with the exact
|
||||
// segment track the ctrl+p status uses; the selected role's chip fills.
|
||||
while (lines.length < rows - 1) lines.push("");
|
||||
|
||||
Reference in New Issue
Block a user