Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/HarnessVersionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface VersionRow extends Record<string, unknown> {
}

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",
Expand Down
60 changes: 58 additions & 2 deletions src/components/PaginatedTablePicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -303,6 +303,62 @@ 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 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"];
Expand Down
2 changes: 1 addition & 1 deletion src/components/RuntimeVersionPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface RuntimeVersionRow extends Record<string, unknown> {
}

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",
Expand Down
40 changes: 18 additions & 22 deletions src/components/ui/data-table/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function DataTable<T extends Record<string, unknown>>({
onNextPage();
}
} else setCurrentPage((p) => Math.min(totalPages - 1, p + 1));
} else if (input === "/") {
} else if (searchable && input === "/") {
setSelectedRow(0);
setCurrentPage(0);
setSearchMode(true);
Expand Down Expand Up @@ -191,26 +191,18 @@ export function DataTable<T extends Record<string, unknown>>({
const width = computedWidths.widths[index];
return width === undefined ? [] : [{ column, width }];
});
const filterLine = searchMode ? (
<Text>
<Text color={theme.colors.primary}>/ Filter: </Text>
<Text color={theme.colors.text}>{searchQuery}</Text>
<Text color={theme.colors.primary}>█</Text>
</Text>
) : searchQuery ? (
<Text color={theme.colors.muted}>/ Filter: {searchQuery}</Text>
) : undefined;

return (
<Box flexDirection="column">
{/* Search bar */}
{searchable && (
<Box marginBottom={0}>
<Text color={theme.colors.muted}>
{searchMode ? (
<Text>
<Text color={theme.colors.primary}>/ Filter: </Text>
<Text color={theme.colors.text}>{searchQuery}</Text>
<Text color={theme.colors.primary}>█</Text>
</Text>
) : searchQuery ? (
<Text color={theme.colors.muted}>/ Filter: {searchQuery}</Text>
) : null}
</Text>
</Box>
)}

{/* Table */}
<Box
flexDirection="column"
Expand All @@ -237,11 +229,15 @@ export function DataTable<T extends Record<string, unknown>>({
))}
</Box>

{/* Divider (or a blank spacer line when hidden) */}
{/* Filter input replaces the divider so filtering does not change table height. */}
<Box flexDirection="row">
<Text color={theme.colors.border}>
{showDivider ? "─".repeat(computedWidths.totalWidth) : " "}
</Text>
{searchable && filterLine ? (
filterLine
) : (
<Text color={theme.colors.border}>
{showDivider ? "─".repeat(computedWidths.totalWidth) : " "}
</Text>
)}
</Box>

{/* Rows */}
Expand Down
31 changes: 8 additions & 23 deletions src/components/ui/data-table/columnWidths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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", () => {
Expand All @@ -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],
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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, {
Expand All @@ -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);
});

Expand Down
38 changes: 25 additions & 13 deletions src/components/ui/key-hint/KeyHint.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,15 +18,24 @@ export interface KeyHintProps {
theme?: InkUITheme;
}

export const KeyHint: React.FC<KeyHintProps> = ({ keys, theme = darkTheme }) => (
<Box gap={2}>
{keys.map(({ key, label }) => (
<Box key={key} gap={1}>
<Text bold dimColor>
[{key}]
</Text>
<Text color={theme.colors.muted}>{label}</Text>
</Box>
))}
</Box>
);
export const KeyHint: React.FC<KeyHintProps> = ({ keys, theme = darkTheme }) => {
const { columns } = useWindowSize();
const fullWidth = stringWidth(keys.map(({ key, label }) => `[${key}] ${label}`).join(HINT_GAP));
const showLabels = fullWidth <= columns;

return (
<Box width={columns}>
<Text wrap="truncate">
{keys.map(({ key, label }, index) => (
<React.Fragment key={`${key}-${label}`}>
{index > 0 ? HINT_GAP : ""}
<Text bold dimColor>
[{key}]
</Text>
{showLabels ? <Text color={theme.colors.muted}> {label}</Text> : null}
</React.Fragment>
))}
</Text>
</Box>
);
};
6 changes: 3 additions & 3 deletions src/components/usePagedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading