From 34de2963e742f4da9560704c0aa79a7c19817300 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Mon, 3 Aug 2026 21:28:30 +0000 Subject: [PATCH 1/2] fix: fill paginated tables to footer --- src/components/PaginatedTablePicker.test.tsx | 31 ++++++++++++++- src/components/ui/data-table/DataTable.tsx | 40 +++++++++----------- src/components/usePagedList.tsx | 6 +-- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/components/PaginatedTablePicker.test.tsx b/src/components/PaginatedTablePicker.test.tsx index f20fb05b1..f824af3e3 100644 --- a/src/components/PaginatedTablePicker.test.tsx +++ b/src/components/PaginatedTablePicker.test.tsx @@ -154,7 +154,7 @@ describe("paginated table picker contract", () => { expect(core.harness.calls.filter((call) => call.method === "listHarnesses")).toEqual([ { method: "listHarnesses", - args: [undefined, 32, { region: "us-east-1", endpointUrl: undefined }], + args: [undefined, 33, { region: "us-east-1", endpointUrl: undefined }], }, ]); await r.press("down"); @@ -163,7 +163,7 @@ describe("paginated table picker contract", () => { await waitForText(r.lastFrame, "❯ page-two-first"); expect(core.harness.calls.at(-1)).toEqual({ method: "listHarnesses", - args: ["t2", 32, { region: "us-east-1", endpointUrl: undefined }], + args: ["t2", 33, { region: "us-east-1", endpointUrl: undefined }], }); await r.press("down"); @@ -303,6 +303,33 @@ describe("paginated table picker contract", () => { expect(r.lastFrame()).toContain("agentcore → harness → get → alpha-1"); }); + test("fills the content area before and during filtering", async () => { + const core = new TestCoreClient(); + core.harness.setListResponse({ + harnesses: Array.from({ length: 33 }, (_, index) => + harness({ + harnessId: `harness-${index}`, + harnessName: `harness-${String(index).padStart(2, "0")}`, + }), + ), + nextToken: "t2", + }); + const r = renderScreen("/agentcore/harness/list", { core }); + + await waitForText(r.lastFrame, "page 1 · more →"); + let lines = (r.lastFrame() ?? "").split("\n"); + expect(lines[37]).toContain("page 1 · more →"); + expect(lines[38]).toMatch(/^─+$/); + + await r.write("/"); + await waitForText(r.lastFrame, "/ Filter:"); + lines = (r.lastFrame() ?? "").split("\n"); + expect(lines[2]).toContain("name"); + expect(lines[3]).toContain("/ Filter:"); + expect(lines[37]).toContain("page 1 · more →"); + expect(lines[38]).toMatch(/^─+$/); + }); + test("keeps rows aligned and single-line while resizing the Runtime table", async () => { const core = new TestCoreClient(); const suffixes = ["AbCdEf1234", "BcDeFg2345", "CdEfGh3456"]; diff --git a/src/components/ui/data-table/DataTable.tsx b/src/components/ui/data-table/DataTable.tsx index f5d337e1f..54c2e726b 100644 --- a/src/components/ui/data-table/DataTable.tsx +++ b/src/components/ui/data-table/DataTable.tsx @@ -143,7 +143,7 @@ export function DataTable>({ onNextPage(); } } else setCurrentPage((p) => Math.min(totalPages - 1, p + 1)); - } else if (input === "/") { + } else if (searchable && input === "/") { setSelectedRow(0); setCurrentPage(0); setSearchMode(true); @@ -191,26 +191,18 @@ export function DataTable>({ const width = computedWidths.widths[index]; return width === undefined ? [] : [{ column, width }]; }); + const filterLine = searchMode ? ( + + / Filter: + {searchQuery} + + + ) : searchQuery ? ( + / Filter: {searchQuery} + ) : undefined; return ( - {/* Search bar */} - {searchable && ( - - - {searchMode ? ( - - / Filter: - {searchQuery} - - - ) : searchQuery ? ( - / Filter: {searchQuery} - ) : null} - - - )} - {/* Table */} >({ ))} - {/* Divider (or a blank spacer line when hidden) */} + {/* Filter input replaces the divider so filtering does not change table height. */} - - {showDivider ? "─".repeat(computedWidths.totalWidth) : " "} - + {searchable && filterLine ? ( + filterLine + ) : ( + + {showDivider ? "─".repeat(computedWidths.totalWidth) : " "} + + )} {/* Rows */} diff --git a/src/components/usePagedList.tsx b/src/components/usePagedList.tsx index fa6ed7a25..3c367c9b9 100644 --- a/src/components/usePagedList.tsx +++ b/src/components/usePagedList.tsx @@ -2,9 +2,9 @@ import { useEffect, useState } from "react"; import { useWindowSize } from "ink"; // CHROME_ROWS is everything a picker screen renders around the table rows: -// the Layout header and footer (2 each), the DataTable filter line, the -// column-header row and its divider, and the pagination status line. -const CHROME_ROWS = 8; +// the Layout header and footer (2 each), the DataTable column-header row and +// divider/filter row, and the pagination status line. +const CHROME_ROWS = 7; export interface PagedList { // pageSize is how many table rows fit the terminal — sent as maxResults so From dc72fb1ee730d193a490c128aa15c53d61573ae6 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Mon, 3 Aug 2026 22:35:38 +0000 Subject: [PATCH 2/2] fix: keep list tables responsive --- src/components/HarnessVersionPicker.tsx | 2 +- src/components/PaginatedTablePicker.test.tsx | 29 ++++++++++++++ src/components/RuntimeVersionPicker.tsx | 2 +- .../ui/data-table/columnWidths.test.ts | 31 ++++----------- src/components/ui/key-hint/KeyHint.tsx | 38 ++++++++++++------- 5 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/components/HarnessVersionPicker.tsx b/src/components/HarnessVersionPicker.tsx index e99f103d3..72af713ce 100644 --- a/src/components/HarnessVersionPicker.tsx +++ b/src/components/HarnessVersionPicker.tsx @@ -15,7 +15,7 @@ interface VersionRow extends Record { } export const harnessVersionColumns = [ - { key: "harnessVersion", header: "version", width: 7 }, + { key: "harnessVersion", header: "version", flex: true }, { key: "status", header: "status", width: 13, minWidth: 6 }, { key: "createdAt", diff --git a/src/components/PaginatedTablePicker.test.tsx b/src/components/PaginatedTablePicker.test.tsx index f824af3e3..349f0acea 100644 --- a/src/components/PaginatedTablePicker.test.tsx +++ b/src/components/PaginatedTablePicker.test.tsx @@ -330,6 +330,35 @@ describe("paginated table picker contract", () => { expect(lines[38]).toMatch(/^─+$/); }); + test("keeps pagination and footer hints below every row at narrow widths", async () => { + const core = new TestCoreClient(); + core.runtime.setListResponse({ + agentRuntimes: Array.from({ length: 17 }, (_, index) => { + const suffix = String(index).padStart(10, "0"); + return runtime({ + agentRuntimeId: `runtime-${suffix}`, + agentRuntimeName: `runtime-${index}`, + }); + }), + nextToken: "t2", + }); + const r = renderScreen("/agentcore/runtime/list", { core }); + + await r.resize(60, 24); + await waitFor(() => { + const frame = r.lastFrame() ?? ""; + return frame.includes("0000000016") && !frame.includes("loading page 1…"); + }); + const lines = (r.lastFrame() ?? "").split("\n"); + + expect(lines).toHaveLength(24); + expect(lines[20]).toContain("0000000016"); + expect(lines[21]).toContain("page 1 · more →"); + expect(lines[22]).toMatch(/^─{60}$/); + expect(lines[23]).toContain("[ctl+c]"); + expect(stringWidth(lines[23]!)).toBeLessThanOrEqual(60); + }); + test("keeps rows aligned and single-line while resizing the Runtime table", async () => { const core = new TestCoreClient(); const suffixes = ["AbCdEf1234", "BcDeFg2345", "CdEfGh3456"]; diff --git a/src/components/RuntimeVersionPicker.tsx b/src/components/RuntimeVersionPicker.tsx index 2989300d7..f589d5a0c 100644 --- a/src/components/RuntimeVersionPicker.tsx +++ b/src/components/RuntimeVersionPicker.tsx @@ -13,7 +13,7 @@ interface RuntimeVersionRow extends Record { } export const runtimeVersionColumns = [ - { key: "version", header: "version", width: 7 }, + { key: "version", header: "version", flex: true }, { key: "status", header: "status", width: 13, minWidth: 6 }, { key: "lastUpdatedAt", diff --git a/src/components/ui/data-table/columnWidths.test.ts b/src/components/ui/data-table/columnWidths.test.ts index c33586727..8ed0bd624 100644 --- a/src/components/ui/data-table/columnWidths.test.ts +++ b/src/components/ui/data-table/columnWidths.test.ts @@ -6,6 +6,7 @@ import { harnessVersionColumns } from "../../HarnessVersionPicker"; import { runtimeEndpointColumns } from "../../RuntimeEndpointPicker"; import { runtimeColumns } from "../../RuntimePicker"; import { runtimeVersionColumns } from "../../RuntimeVersionPicker"; +import { memoryColumns } from "../../../handlers/memory/list/screen"; import { computeColumnWidths, FLEX_MIN_WIDTH, @@ -19,10 +20,9 @@ const flexConfigs = [ { name: "Harness", columns: harnessColumns, flexIndex: 0 }, { name: "Harness endpoint", columns: harnessEndpointColumns, flexIndex: 0 }, { name: "Runtime endpoint", columns: runtimeEndpointColumns, flexIndex: 0 }, -] as const; -const fixedConfigs = [ - { name: "Runtime version", columns: runtimeVersionColumns }, - { name: "Harness version", columns: harnessVersionColumns }, + { name: "Runtime version", columns: runtimeVersionColumns, flexIndex: 0 }, + { name: "Harness version", columns: harnessVersionColumns, flexIndex: 0 }, + { name: "Memory", columns: memoryColumns, flexIndex: 0 }, ] as const; describe("computeColumnWidths", () => { @@ -40,21 +40,6 @@ describe("computeColumnWidths", () => { }); } - for (const config of fixedConfigs) { - test(`${config.name} remains content-sized when space is available`, () => { - for (const terminalWidth of widths) { - const result = computeColumnWidths(config.columns, terminalWidth, { - selectable: true, - borderWidth: 0, - }); - - expect(result.totalWidth).toBe(40); - expect(result.totalWidth).toBeLessThanOrEqual(terminalWidth); - expect(result.widths.every((width) => width !== undefined)).toBe(true); - } - }); - } - test("drops fixed columns from right to left without truncating headers", () => { expect(computeColumnWidths(runtimeColumns, 40, { selectable: true, borderWidth: 0 })).toEqual({ widths: [19, 10, 7, undefined, undefined], @@ -84,7 +69,7 @@ describe("computeColumnWidths", () => { }); test("keeps every visible production header intact", () => { - for (const config of [...flexConfigs, ...fixedConfigs]) { + for (const config of flexConfigs) { for (let terminalWidth = 1; terminalWidth <= 200; terminalWidth += 1) { const result = computeColumnWidths(config.columns, terminalWidth, { selectable: true, @@ -120,7 +105,7 @@ describe("computeColumnWidths", () => { }); test("terminates and respects the documented narrow-terminal bound", () => { - for (const config of [...flexConfigs, ...fixedConfigs]) { + for (const config of flexConfigs) { const hasFlex = config.columns.some((column) => "flex" in column && column.flex === true); for (let terminalWidth = 1; terminalWidth <= 20; terminalWidth += 1) { const result = computeColumnWidths(config.columns, terminalWidth, { @@ -137,12 +122,12 @@ describe("computeColumnWidths", () => { }); test("does not reserve a phantom gap after every data column drops", () => { - const result = computeColumnWidths(runtimeVersionColumns, 1, { + const result = computeColumnWidths([{ width: 6 }], 1, { selectable: true, borderWidth: 0, }); - expect(result.widths).toEqual([undefined, undefined, undefined]); + expect(result.widths).toEqual([undefined]); expect(result.totalWidth).toBe(SELECTION_MARKER_WIDTH); }); diff --git a/src/components/ui/key-hint/KeyHint.tsx b/src/components/ui/key-hint/KeyHint.tsx index 6f0e80086..11c5610e2 100644 --- a/src/components/ui/key-hint/KeyHint.tsx +++ b/src/components/ui/key-hint/KeyHint.tsx @@ -1,8 +1,11 @@ import React from "react"; -import { Text, Box } from "ink"; +import { Box, Text, useWindowSize } from "ink"; +import stringWidth from "string-width"; import { darkTheme } from "../_core.js"; import type { InkUITheme } from "../_core.js"; +const HINT_GAP = " "; + export interface KeyHintItem { /** Displayed in brackets, e.g. "Enter", "↑↓", "Space" */ key: string; @@ -15,15 +18,24 @@ export interface KeyHintProps { theme?: InkUITheme; } -export const KeyHint: React.FC = ({ keys, theme = darkTheme }) => ( - - {keys.map(({ key, label }) => ( - - - [{key}] - - {label} - - ))} - -); +export const KeyHint: React.FC = ({ keys, theme = darkTheme }) => { + const { columns } = useWindowSize(); + const fullWidth = stringWidth(keys.map(({ key, label }) => `[${key}] ${label}`).join(HINT_GAP)); + const showLabels = fullWidth <= columns; + + return ( + + + {keys.map(({ key, label }, index) => ( + + {index > 0 ? HINT_GAP : ""} + + [{key}] + + {showLabels ? {label} : null} + + ))} + + + ); +};