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 f20fb05b1..6069d3119 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,66 @@ describe("paginated table picker contract", () => { expect(r.lastFrame()).toContain("agentcore → harness → get → alpha-1"); }); + test("fills the content area before and during a long filter", async () => { + const longFilter = `${"filter-prefix-".repeat(8)}visible-suffix`; + const core = new TestCoreClient(); + core.harness.setListResponse({ + harnesses: Array.from({ length: 33 }, (_, index) => + harness({ + harnessId: `harness-${index}`, + harnessName: `${longFilter}-${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 r.write(longFilter); + await waitForText(r.lastFrame, "visible-suffix"); + lines = (r.lastFrame() ?? "").split("\n"); + expect(lines[2]).toContain("name"); + expect(lines[3]).toContain("/ Filter: …"); + expect(lines[3]).toContain("visible-suffix█"); + expect(stringWidth(lines[3]!)).toBeLessThanOrEqual(100); + expect(lines[37]).toContain("page 1 · more →"); + 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("[esc] back"); + 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/DataTable.tsx b/src/components/ui/data-table/DataTable.tsx index f5d337e1f..2d9745fbe 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,33 @@ export function DataTable>({ const width = computedWidths.widths[index]; return width === undefined ? [] : [{ column, width }]; }); + const filterPrefix = "/ Filter: "; + const filterCursor = searchMode ? "█" : ""; + const availableQueryWidth = Math.max( + 0, + computedWidths.totalWidth - stringWidth(filterPrefix) - stringWidth(filterCursor), + ); + const visibleSearchQuery = + availableQueryWidth > 0 + ? cliTruncate(searchQuery, availableQueryWidth, { + position: "start", + }) + : ""; + const filterLine = searchMode ? ( + + {filterPrefix} + {visibleSearchQuery} + {filterCursor} + + ) : searchQuery ? ( + + {filterPrefix} + {visibleSearchQuery} + + ) : undefined; return ( - {/* Search bar */} - {searchable && ( - - - {searchMode ? ( - - / Filter: - {searchQuery} - - - ) : searchQuery ? ( - / Filter: {searchQuery} - ) : null} - - - )} - {/* Table */} >({ ))} - {/* Divider (or a blank spacer line when hidden) */} - - - {showDivider ? "─".repeat(computedWidths.totalWidth) : " "} - + {/* Filter input replaces the divider so filtering does not change table height. */} + + {searchable && filterLine ? ( + filterLine + ) : ( + + {showDivider ? "─".repeat(computedWidths.totalWidth) : " "} + + )} {/* Rows */} 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/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