diff --git a/src/components/PaginatedTablePicker.test.tsx b/src/components/PaginatedTablePicker.test.tsx index 6069d3119..708e7a461 100644 --- a/src/components/PaginatedTablePicker.test.tsx +++ b/src/components/PaginatedTablePicker.test.tsx @@ -180,6 +180,29 @@ describe("paginated table picker contract", () => { ).toBe(false); }); + test("restores the selected row after returning from its detail screen", async () => { + const selected = harness({ harnessName: "third-harness", harnessId: "third-harness" }); + const core = coreWith([ + harness({ harnessName: "first-harness", harnessId: "first-harness" }), + harness({ harnessName: "second-harness", harnessId: "second-harness" }), + selected, + ]); + core.harness.setGetResponse(getResponse(selected)); + const r = renderScreen("/agentcore/harness/list", { core }); + + await waitForText(r.lastFrame, "third-harness"); + await r.press("down"); + await r.press("down"); + await waitForText(r.lastFrame, "❯ third-harness"); + await r.press("return"); + await waitForText(r.lastFrame, "agentcore → harness → get → third-harness"); + + await r.press("escape"); + await waitForText(r.lastFrame, "updated UTC"); + expect(r.lastFrame()).toContain("❯ third-harness"); + expect(r.lastFrame()).not.toContain("❯ first-harness"); + }); + test("retains rows and disables selection and paging during a transition", async () => { const core = new TestCoreClient(); core.harness.setListResponse({ diff --git a/src/components/PaginatedTablePicker.tsx b/src/components/PaginatedTablePicker.tsx index 8b373f158..b915dd49d 100644 --- a/src/components/PaginatedTablePicker.tsx +++ b/src/components/PaginatedTablePicker.tsx @@ -1,5 +1,6 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { Text, useInput } from "ink"; +import { useLocation, useNavigate } from "react-router"; import { Layout } from "./Layout"; import { usePagedList } from "./usePagedList"; import { darkTheme } from "./ui/_core.js"; @@ -11,6 +12,10 @@ export interface TokenPage { nextToken?: string; } +interface PaginatedTableLocationState extends Record { + paginatedTableSelection?: string; +} + export interface PaginatedTablePickerProps> { breadcrumb: string[]; description?: string; @@ -46,6 +51,8 @@ export function PaginatedTablePicker emptyPageMessage, maxPageSize, }: PaginatedTablePickerProps) { + const location = useLocation(); + const navigate = useNavigate(); const paging = usePagedList(maxPageSize); const list = useQuery({ queryKey: [...queryKey, paging.pageSize, paging.token], @@ -57,6 +64,16 @@ export function PaginatedTablePicker const pageTransition = list.isFetching && !list.isPending; const mappedRows = (list.data?.items ?? []).map(toRow); const rows = sortRows ? sortRows(mappedRows) : mappedRows; + const locationState = + location.state && typeof location.state === "object" + ? (location.state as PaginatedTableLocationState) + : {}; + const initialSelectedRow = locationState.paginatedTableSelection + ? Math.max( + 0, + rows.findIndex((row) => getValue(row) === locationState.paginatedTableSelection), + ) + : 0; useInput( (input, key) => { @@ -106,6 +123,7 @@ export function PaginatedTablePicker showFooter={false} showDivider={true} pageSize={paging.pageSize} + initialSelectedRow={initialSelectedRow} selectionResetKey={paging.pageSize} focus={!pageTransition} columns={columns} @@ -113,7 +131,19 @@ export function PaginatedTablePicker emptyMessage={paginated ? emptyPageMessage : emptyMessage} onSelect={(row) => { const value = getValue(row); - if (value) onSelect(value); + if (!value) return; + navigate( + { + pathname: location.pathname, + search: location.search, + hash: location.hash, + }, + { + replace: true, + state: { ...locationState, paginatedTableSelection: value }, + }, + ); + onSelect(value); }} onEscape={onBack} onPrevPage={!pageTransition && paging.pageIndex > 0 ? paging.prev : undefined} diff --git a/src/components/ui/data-table/DataTable.tsx b/src/components/ui/data-table/DataTable.tsx index 2d9745fbe..b0ccb02d7 100644 --- a/src/components/ui/data-table/DataTable.tsx +++ b/src/components/ui/data-table/DataTable.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import cliTruncate from "cli-truncate"; import { Box, Text, useInput, useWindowSize } from "ink"; import stringWidth from "string-width"; @@ -47,6 +47,7 @@ export interface DataTableProps { showFooter?: boolean; emptyMessage?: string; focus?: boolean; + initialSelectedRow?: number; selectionResetKey?: string | number; theme?: InkUITheme; } @@ -70,16 +71,20 @@ export function DataTable>({ showFooter = true, emptyMessage = "No data", focus = true, + initialSelectedRow = 0, selectionResetKey, theme = darkTheme, }: DataTableProps): React.ReactElement { const { columns: terminalWidth } = useWindowSize(); - const [selectedRow, setSelectedRow] = useState(0); + const [selectedRow, setSelectedRow] = useState(() => Math.max(0, initialSelectedRow)); const [currentPage, setCurrentPage] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [searchMode, setSearchMode] = useState(false); + const selectionResetKeyRef = useRef(selectionResetKey); useEffect(() => { + if (selectionResetKeyRef.current === selectionResetKey) return; + selectionResetKeyRef.current = selectionResetKey; setSelectedRow(0); setCurrentPage(0); }, [selectionResetKey]); diff --git a/src/handlers/memory/event/event.screen.test.tsx b/src/handlers/memory/event/event.screen.test.tsx index 76f42fdea..337b7b082 100644 --- a/src/handlers/memory/event/event.screen.test.tsx +++ b/src/handlers/memory/event/event.screen.test.tsx @@ -177,6 +177,30 @@ describe("Memory event list flow", () => { }); }); + test("restores the selected Event after returning from its JSON", async () => { + const selectedEvent = event({ eventId: "event-3" }); + const core = new TestCoreClient(); + core.memory.setListEventsResponse({ + events: [event({ eventId: "event-1" }), event({ eventId: "event-2" }), selectedEvent], + }); + core.memory.setGetEventResponse({ event: selectedEvent }); + const screen = renderScreen("/agentcore/memory/event/list/memory-1/actor-1/session-1", { + core, + }); + + await waitForText(screen.lastFrame, "event-3"); + await screen.press("down"); + await screen.press("down"); + await waitForText(screen.lastFrame, "❯ event-3"); + await screen.press("return"); + await waitForText(screen.lastFrame, '"eventId": "event-3"'); + + await screen.press("escape"); + await waitForText(screen.lastFrame, "occurred UTC"); + expect(screen.lastFrame()).toContain("❯ event-3"); + expect(screen.lastFrame()).not.toContain("❯ event-1"); + }); + test("paginates events and distinguishes a later-page empty state", async () => { const core = new TestCoreClient(); core.memory.setListEventsResponse({