Skip to content

Commit 272e50e

Browse files
authored
feat: add Runtime read-only TUI (#1802)
* feat: add Runtime TUI route menus * test: add controllable Runtime screen client * test: focus Runtime test helper coverage * feat: add Runtime TUI picker * test: stabilize screen resize synchronization * feat: add Runtime TUI hub and detail * feat: add Runtime version TUI * feat: add Runtime endpoint TUI * feat: add explicit Runtime TUI entry * refactor: keep test IO options local * test: complete Runtime TUI coverage * test: verify Runtime TUI exit and filtering * fix: stabilize Runtime pagination resets * fix: keep TUI footer stable in narrow terminals * fix: clarify Runtime latest version column * fix: align Runtime TUI navigation with Harness * refactor: simplify Runtime TUI helpers * fix: align Runtime TUI entry and detail output * test: simplify Runtime TUI integration coverage * refactor: share token-paged table picker * fix: strip SDK metadata from Runtime output * fix: support pasted table filters * test: stabilize Runtime TUI exit readiness * test: decouple Runtime exit readiness from rendering * refactor: align Runtime picker component placement * test: replace fixed TUI delays with state waits * refactor: align Runtime route registration with Harness * refactor: clarify Harness picker names * refactor: keep table filter copy generic * test: remove redundant DataTable copy test * refactor: separate table reset responsibilities * refactor: simplify key hint width calculation * test: consolidate runtime TUI coverage * refactor: simplify paged picker implementation * fix: preserve Winston dependencies after rebase * refactor: defer SDK metadata filtering * docs: use TSDoc for Harness pickers * refactor: rename paginated table picker
1 parent 3bf082c commit 272e50e

46 files changed

Lines changed: 2725 additions & 643 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ It gives you two ways to work, from the same binary:
99
- **A scriptable CLI** — every operation is a flag-driven subcommand that emits
1010
JSON (`--json`), so it can be used by codeing agents and can drop cleanly into
1111
scripts, CI, and automation.
12-
- **An interactive TUI** — run a command with no arguments and it opens a
13-
full-screen terminal app for browsing resources, filling in wizards, and
14-
chatting with a live agent.
12+
- **An interactive TUI** — bare Harness and Runtime branches and leaves open
13+
their corresponding menus and selection flows.
1514

1615
```bash
1716
agentcore # launch the interactive TUI
@@ -27,8 +26,8 @@ responses. `agentcore` wraps all of that behind one ergonomic tool.
2726

2827
## Command surface
2928

30-
Every leaf command runs headless with flags, or opens the matching TUI screen
31-
when invoked bare.
29+
Commands with operation flags run headlessly. Bare Harness and Runtime branches
30+
and leaves open their interactive flows.
3231

3332
```
3433
agentcore # interactive TUI
@@ -117,6 +116,18 @@ agentcore identity api-key-credential-provider update --name my-provider --api-k
117116
agentcore identity api-key-credential-provider delete --name my-provider
118117
```
119118

119+
Bare Runtime branches and leaves require a TTY on stdin and stdout. Supplying
120+
operation flags runs the command headlessly, and `--json` always suppresses TUI
121+
rendering.
122+
123+
```bash
124+
agentcore runtime
125+
agentcore runtime list
126+
agentcore runtime get
127+
agentcore runtime version list
128+
agentcore runtime endpoint list
129+
```
130+
120131
---
121132

122133
# Architecture & patterns

src/components/EndpointPicker.tsx

Lines changed: 0 additions & 129 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useNavigate } from "react-router";
2+
import type { HarnessEndpoint } from "@aws-sdk/client-bedrock-agentcore-control";
3+
import type { ScreenProps } from "../handlers/types";
4+
import { coreOptsFromCtx } from "../handlers/utils";
5+
import { PaginatedTablePicker } from "./PaginatedTablePicker";
6+
7+
// EndpointRow is the flat, display-ready shape the table renders.
8+
interface EndpointRow extends Record<string, unknown> {
9+
endpointName: string;
10+
liveVersion: string;
11+
targetVersion: string;
12+
status: string;
13+
updatedAt: string;
14+
}
15+
16+
function toRow(e: HarnessEndpoint): EndpointRow {
17+
return {
18+
endpointName: e.endpointName!,
19+
liveVersion: e.liveVersion ?? "-",
20+
targetVersion: e.targetVersion ?? "-",
21+
status: e.status!,
22+
updatedAt: e.updatedAt!.toISOString(),
23+
};
24+
}
25+
26+
export interface HarnessEndpointPickerProps extends ScreenProps {
27+
// harnessId scopes the listing to one harness's endpoints.
28+
harnessId: string;
29+
// breadcrumb labels the screen the picker is serving.
30+
breadcrumb: string[];
31+
// description tells the user what selecting an endpoint will do.
32+
description?: string;
33+
// onSelect receives the chosen endpoint's name.
34+
onSelect: (endpointName: string) => void;
35+
// onEscape overrides what esc does (default: pop back in history). Hosts
36+
// that embed the picker as an overlay (e.g. the chat's ctrl+t endpoint
37+
// switch) pass a closer instead.
38+
onEscape?: () => void;
39+
}
40+
41+
/**
42+
* Fetches a harness's endpoints and renders them as a navigable table.
43+
*
44+
* This is the endpoint counterpart of HarnessPicker, shared by every "pick an
45+
* endpoint" screen (list, update, delete). Esc pops back.
46+
*/
47+
export function HarnessEndpointPicker({
48+
ctx,
49+
core,
50+
harnessId,
51+
breadcrumb,
52+
description,
53+
onSelect,
54+
onEscape,
55+
}: HarnessEndpointPickerProps) {
56+
const opts = coreOptsFromCtx(ctx);
57+
const navigate = useNavigate();
58+
const goBack = onEscape ?? (() => navigate(-1));
59+
60+
return (
61+
<PaginatedTablePicker
62+
breadcrumb={breadcrumb}
63+
description={description}
64+
queryKey={["harness-endpoints", opts.region, harnessId]}
65+
loadPage={async (token, pageSize) => {
66+
const response = await core.harness.listHarnessEndpoints(harnessId, token, pageSize, opts);
67+
return {
68+
items: response.endpoints ?? [],
69+
nextToken: response.nextToken,
70+
};
71+
}}
72+
toRow={toRow}
73+
columns={[
74+
{ key: "endpointName", header: "name" },
75+
{ key: "liveVersion", header: "live" },
76+
{ key: "targetVersion", header: "target" },
77+
{ key: "status", header: "status" },
78+
{ key: "updatedAt", header: "updatedAt" },
79+
]}
80+
getValue={(row) => row.endpointName}
81+
onSelect={onSelect}
82+
onBack={goBack}
83+
loadingMessage="Loading endpoints…"
84+
errorMessage={(error) => `Error: ${error.message}`}
85+
emptyMessage="This harness has no endpoints yet."
86+
emptyPageMessage={`No endpoints on this page for harness ${harnessId}.`}
87+
/>
88+
);
89+
}

src/components/HarnessPicker.tsx

Lines changed: 32 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import { Text, useWindowSize } from "ink";
2-
import { useQuery, keepPreviousData } from "@tanstack/react-query";
31
import { useNavigate } from "react-router";
42
import type { HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control";
5-
import { DataTable } from "./ui/data-table";
63
import type { ScreenProps } from "../handlers/types";
74
import { coreOptsFromCtx } from "../handlers/utils";
8-
import { usePagedList } from "./usePagedList";
9-
import { Spinner } from "./ui/spinner";
10-
import { Layout } from "./Layout";
11-
import { darkTheme } from "./ui/_core.js";
5+
import { PaginatedTablePicker } from "./PaginatedTablePicker";
126

137
// HarnessRow is the flat, display-ready shape the table renders. It also satisfies
148
// DataTable's `T extends Record<string, unknown>` constraint, which the SDK's
@@ -43,11 +37,13 @@ export interface HarnessPickerProps extends ScreenProps {
4337
onSelect: (harnessId: string) => void;
4438
}
4539

46-
// HarnessPicker fetches the caller's harnesses and renders them as a navigable
47-
// table. It is the shared body of every "pick a harness" screen (list, invoke);
48-
// hosts differ only in breadcrumb, subtitle, and what selection does. Esc
49-
// returns to the parent menu, derived from the breadcrumb (e.g. the endpoint
50-
// menu for [..., "endpoint", "list"]).
40+
/**
41+
* Fetches the caller's harnesses and renders them as a navigable table.
42+
*
43+
* This is the shared body of every "pick a harness" screen (list, invoke);
44+
* hosts differ only in breadcrumb, subtitle, and what selection does. Esc
45+
* returns to the parent menu derived from the breadcrumb.
46+
*/
5147
export function HarnessPicker({
5248
ctx,
5349
core,
@@ -56,69 +52,35 @@ export function HarnessPicker({
5652
onSelect,
5753
}: HarnessPickerProps) {
5854
const opts = coreOptsFromCtx(ctx);
59-
const { columns } = useWindowSize();
6055
const navigate = useNavigate();
61-
const paging = usePagedList();
62-
63-
const list = useQuery({
64-
queryKey: ["harnesses", opts.region, paging.pageSize, paging.token],
65-
queryFn: () => core.harness.listHarnesses(paging.token, paging.pageSize, opts),
66-
placeholderData: keepPreviousData,
67-
});
68-
69-
const nextToken = list.data?.nextToken;
70-
// Pagination surfaces only once a response reports more pages (nextToken).
71-
const paginated = paging.pageIndex > 0 || nextToken !== undefined;
56+
const goBack = () => navigate("/" + breadcrumb.slice(0, -1).join("/"));
7257

7358
return (
74-
<Layout
59+
<PaginatedTablePicker
7560
breadcrumb={breadcrumb}
7661
description={description}
77-
keyHints={[
78-
{ key: "↑↓/kj", label: "navigate" },
79-
...(paginated ? [{ key: "←→/hl", label: "page" }] : []),
80-
{ key: "/", label: "filter" },
81-
{ key: "enter", label: "select" },
82-
{ key: "esc", label: "back" },
83-
{ key: "ctl+c", label: "quit" },
62+
queryKey={["harnesses", opts.region]}
63+
loadPage={async (token, pageSize) => {
64+
const response = await core.harness.listHarnesses(token, pageSize, opts);
65+
return {
66+
items: response.harnesses ?? [],
67+
nextToken: response.nextToken,
68+
};
69+
}}
70+
toRow={toRow}
71+
columns={[
72+
{ key: "harnessName", header: "name" },
73+
{ key: "harnessVersion", header: "version" },
74+
{ key: "status", header: "status" },
75+
{ key: "updatedAt", header: "updatedAt" },
8476
]}
85-
>
86-
{list.isPending ? (
87-
<Spinner label="Loading harnesses…" />
88-
) : list.isError ? (
89-
<Text color="red">Error: {(list.error as Error).message}</Text>
90-
) : (
91-
<>
92-
<DataTable
93-
borderStyle="none"
94-
borderTop={false}
95-
borderBottom={false}
96-
borderRight={false}
97-
showFooter={false}
98-
showDivider={true}
99-
pageSize={paging.pageSize}
100-
columns={[
101-
{ key: "harnessName", header: "name", width: columns - 62 },
102-
{ key: "updatedAt", header: "updatedAt", width: 30 },
103-
{ key: "harnessVersion", header: "version", width: 10 },
104-
{ key: "status", header: "status", width: 20 },
105-
]}
106-
data={(list.data.harnesses ?? []).map(toRow)}
107-
onSelect={(row) => {
108-
if (row.harnessId) onSelect(row.harnessId);
109-
}}
110-
onEscape={() => navigate("/" + breadcrumb.slice(0, -1).join("/"))}
111-
onPrevPage={paging.pageIndex > 0 ? paging.prev : undefined}
112-
onNextPage={nextToken ? () => paging.next(nextToken) : undefined}
113-
/>
114-
{paginated && (
115-
<Text color={darkTheme.colors.muted} dimColor>
116-
page {paging.pageIndex + 1}
117-
{nextToken ? " · more →" : ""}
118-
</Text>
119-
)}
120-
</>
121-
)}
122-
</Layout>
77+
getValue={(row) => row.harnessId}
78+
onSelect={onSelect}
79+
onBack={goBack}
80+
loadingMessage="Loading harnesses…"
81+
errorMessage={(error) => `Error: ${error.message}`}
82+
emptyMessage="No harnesses found."
83+
emptyPageMessage="No harnesses on this page."
84+
/>
12385
);
12486
}

0 commit comments

Comments
 (0)