Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2cd8593
feat: add Runtime TUI route menus
aidandaly24 Jul 21, 2026
b95036a
test: add controllable Runtime screen client
aidandaly24 Jul 21, 2026
410de77
test: focus Runtime test helper coverage
aidandaly24 Jul 21, 2026
2575049
feat: add Runtime TUI picker
aidandaly24 Jul 21, 2026
506fef3
test: stabilize screen resize synchronization
aidandaly24 Jul 21, 2026
95bd4c1
feat: add Runtime TUI hub and detail
aidandaly24 Jul 21, 2026
1a3bb94
feat: add Runtime version TUI
aidandaly24 Jul 21, 2026
79301bb
feat: add Runtime endpoint TUI
aidandaly24 Jul 21, 2026
801f608
feat: add explicit Runtime TUI entry
aidandaly24 Jul 21, 2026
739efc8
refactor: keep test IO options local
aidandaly24 Jul 21, 2026
6d34438
test: complete Runtime TUI coverage
aidandaly24 Jul 21, 2026
76cb020
test: verify Runtime TUI exit and filtering
aidandaly24 Jul 21, 2026
3028bbf
fix: stabilize Runtime pagination resets
aidandaly24 Jul 21, 2026
2735339
fix: keep TUI footer stable in narrow terminals
aidandaly24 Jul 21, 2026
5ec22d0
fix: clarify Runtime latest version column
aidandaly24 Jul 21, 2026
451c27d
fix: align Runtime TUI navigation with Harness
aidandaly24 Jul 21, 2026
e81653c
refactor: simplify Runtime TUI helpers
aidandaly24 Jul 22, 2026
dbbe455
fix: align Runtime TUI entry and detail output
aidandaly24 Jul 22, 2026
a25dd75
test: simplify Runtime TUI integration coverage
aidandaly24 Jul 22, 2026
6b7e4a2
refactor: share token-paged table picker
aidandaly24 Jul 22, 2026
b98fdac
fix: strip SDK metadata from Runtime output
aidandaly24 Jul 22, 2026
9f4bfa0
fix: support pasted table filters
aidandaly24 Jul 22, 2026
07bee92
test: stabilize Runtime TUI exit readiness
aidandaly24 Jul 22, 2026
35118b6
test: decouple Runtime exit readiness from rendering
aidandaly24 Jul 22, 2026
e44e5e4
refactor: align Runtime picker component placement
aidandaly24 Jul 22, 2026
06c39f0
test: replace fixed TUI delays with state waits
aidandaly24 Jul 22, 2026
97f175c
refactor: align Runtime route registration with Harness
aidandaly24 Jul 22, 2026
eaf3a2f
refactor: clarify Harness picker names
aidandaly24 Jul 22, 2026
b1f1dac
refactor: keep table filter copy generic
aidandaly24 Jul 22, 2026
0b36541
test: remove redundant DataTable copy test
aidandaly24 Jul 22, 2026
4fed8f0
refactor: separate table reset responsibilities
aidandaly24 Jul 22, 2026
70e8350
refactor: simplify key hint width calculation
aidandaly24 Jul 22, 2026
0b11c1f
test: consolidate runtime TUI coverage
aidandaly24 Jul 22, 2026
88834c4
refactor: simplify paged picker implementation
aidandaly24 Jul 23, 2026
8c9ec77
fix: preserve Winston dependencies after rebase
aidandaly24 Jul 23, 2026
0b46082
refactor: defer SDK metadata filtering
aidandaly24 Jul 23, 2026
432a397
docs: use TSDoc for Harness pickers
aidandaly24 Jul 23, 2026
4ffe40a
refactor: rename paginated table picker
aidandaly24 Jul 23, 2026
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
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ It gives you two ways to work, from the same binary:
- **A scriptable CLI** — every operation is a flag-driven subcommand that emits
JSON (`--json`), so it can be used by codeing agents and can drop cleanly into
scripts, CI, and automation.
- **An interactive TUI** — run a command with no arguments and it opens a
full-screen terminal app for browsing resources, filling in wizards, and
chatting with a live agent.
- **An interactive TUI** — bare Harness and Runtime branches and leaves open
their corresponding menus and selection flows.

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

## Command surface

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

```
agentcore # interactive TUI
Expand Down Expand Up @@ -117,6 +116,18 @@ agentcore identity api-key-credential-provider update --name my-provider --api-k
agentcore identity api-key-credential-provider delete --name my-provider
```

Bare Runtime branches and leaves require a TTY on stdin and stdout. Supplying
operation flags runs the command headlessly, and `--json` always suppresses TUI
rendering.

```bash
agentcore runtime
agentcore runtime list
agentcore runtime get
agentcore runtime version list
agentcore runtime endpoint list
```

---

# Architecture & patterns
Expand Down
129 changes: 0 additions & 129 deletions src/components/EndpointPicker.tsx

This file was deleted.

89 changes: 89 additions & 0 deletions src/components/HarnessEndpointPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useNavigate } from "react-router";

@aidandaly24 aidandaly24 Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't be against sorting these into folders since I could picture this getting unwieldy as time goes on. The reason I held off was because I didn't know if maybe we just sort the primitive ones or if maybe we just want to sort into a folder called "pickers".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually prefer a flat directory of components! I tend to think a flat directory is easier to maintain over time and that it makes it easier to find what you're looking for.

import type { HarnessEndpoint } from "@aws-sdk/client-bedrock-agentcore-control";
import type { ScreenProps } from "../handlers/types";
import { coreOptsFromCtx } from "../handlers/utils";
import { PaginatedTablePicker } from "./PaginatedTablePicker";

// EndpointRow is the flat, display-ready shape the table renders.
interface EndpointRow extends Record<string, unknown> {
endpointName: string;
liveVersion: string;
targetVersion: string;
status: string;
updatedAt: string;
}

function toRow(e: HarnessEndpoint): EndpointRow {
return {
endpointName: e.endpointName!,
liveVersion: e.liveVersion ?? "-",
targetVersion: e.targetVersion ?? "-",
status: e.status!,
updatedAt: e.updatedAt!.toISOString(),
};
}

export interface HarnessEndpointPickerProps extends ScreenProps {
// harnessId scopes the listing to one harness's endpoints.
harnessId: string;
// breadcrumb labels the screen the picker is serving.
breadcrumb: string[];
// description tells the user what selecting an endpoint will do.
description?: string;
// onSelect receives the chosen endpoint's name.
onSelect: (endpointName: string) => void;
// onEscape overrides what esc does (default: pop back in history). Hosts
// that embed the picker as an overlay (e.g. the chat's ctrl+t endpoint
// switch) pass a closer instead.
onEscape?: () => void;
}

/**
* Fetches a harness's endpoints and renders them as a navigable table.
*
* This is the endpoint counterpart of HarnessPicker, shared by every "pick an
* endpoint" screen (list, update, delete). Esc pops back.
*/
export function HarnessEndpointPicker({
ctx,
core,
harnessId,
breadcrumb,
description,
onSelect,
onEscape,
}: HarnessEndpointPickerProps) {
const opts = coreOptsFromCtx(ctx);
const navigate = useNavigate();
const goBack = onEscape ?? (() => navigate(-1));

return (
<PaginatedTablePicker
breadcrumb={breadcrumb}
description={description}
queryKey={["harness-endpoints", opts.region, harnessId]}
loadPage={async (token, pageSize) => {
const response = await core.harness.listHarnessEndpoints(harnessId, token, pageSize, opts);
return {
items: response.endpoints ?? [],
nextToken: response.nextToken,
};
}}
toRow={toRow}
columns={[
{ key: "endpointName", header: "name" },
{ key: "liveVersion", header: "live" },
{ key: "targetVersion", header: "target" },
{ key: "status", header: "status" },
{ key: "updatedAt", header: "updatedAt" },
]}
getValue={(row) => row.endpointName}
onSelect={onSelect}
onBack={goBack}
loadingMessage="Loading endpoints…"
errorMessage={(error) => `Error: ${error.message}`}
emptyMessage="This harness has no endpoints yet."
emptyPageMessage={`No endpoints on this page for harness ${harnessId}.`}
/>
);
}
102 changes: 32 additions & 70 deletions src/components/HarnessPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { Text, useWindowSize } from "ink";
import { useQuery, keepPreviousData } from "@tanstack/react-query";
import { useNavigate } from "react-router";
import type { HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control";
import { DataTable } from "./ui/data-table";
import type { ScreenProps } from "../handlers/types";
import { coreOptsFromCtx } from "../handlers/utils";
import { usePagedList } from "./usePagedList";
import { Spinner } from "./ui/spinner";
import { Layout } from "./Layout";
import { darkTheme } from "./ui/_core.js";
import { PaginatedTablePicker } from "./PaginatedTablePicker";

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

// HarnessPicker fetches the caller's harnesses and renders them as a navigable
// table. It is the shared body of every "pick a harness" screen (list, invoke);
// hosts differ only in breadcrumb, subtitle, and what selection does. Esc
// returns to the parent menu, derived from the breadcrumb (e.g. the endpoint
// menu for [..., "endpoint", "list"]).
/**
* Fetches the caller's harnesses and renders them as a navigable table.
*
* This is the shared body of every "pick a harness" screen (list, invoke);
* hosts differ only in breadcrumb, subtitle, and what selection does. Esc
* returns to the parent menu derived from the breadcrumb.
*/
export function HarnessPicker({
ctx,
core,
Expand All @@ -56,69 +52,35 @@ export function HarnessPicker({
onSelect,
}: HarnessPickerProps) {
const opts = coreOptsFromCtx(ctx);
const { columns } = useWindowSize();
const navigate = useNavigate();
const paging = usePagedList();

const list = useQuery({
queryKey: ["harnesses", opts.region, paging.pageSize, paging.token],
queryFn: () => core.harness.listHarnesses(paging.token, paging.pageSize, opts),
placeholderData: keepPreviousData,
});

const nextToken = list.data?.nextToken;
// Pagination surfaces only once a response reports more pages (nextToken).
const paginated = paging.pageIndex > 0 || nextToken !== undefined;
const goBack = () => navigate("/" + breadcrumb.slice(0, -1).join("/"));

return (
<Layout
<PaginatedTablePicker
breadcrumb={breadcrumb}
description={description}
keyHints={[
{ key: "↑↓/kj", label: "navigate" },
...(paginated ? [{ key: "←→/hl", label: "page" }] : []),
{ key: "/", label: "filter" },
{ key: "enter", label: "select" },
{ key: "esc", label: "back" },
{ key: "ctl+c", label: "quit" },
queryKey={["harnesses", opts.region]}
loadPage={async (token, pageSize) => {
const response = await core.harness.listHarnesses(token, pageSize, opts);
return {
items: response.harnesses ?? [],
nextToken: response.nextToken,
};
}}
toRow={toRow}
columns={[
{ key: "harnessName", header: "name" },
{ key: "harnessVersion", header: "version" },
{ key: "status", header: "status" },
{ key: "updatedAt", header: "updatedAt" },
]}
>
{list.isPending ? (
<Spinner label="Loading harnesses…" />
) : list.isError ? (
<Text color="red">Error: {(list.error as Error).message}</Text>
) : (
<>
<DataTable
borderStyle="none"
borderTop={false}
borderBottom={false}
borderRight={false}
showFooter={false}
showDivider={true}
pageSize={paging.pageSize}
columns={[
{ key: "harnessName", header: "name", width: columns - 62 },
{ key: "updatedAt", header: "updatedAt", width: 30 },
{ key: "harnessVersion", header: "version", width: 10 },
{ key: "status", header: "status", width: 20 },
]}
data={(list.data.harnesses ?? []).map(toRow)}
onSelect={(row) => {
if (row.harnessId) onSelect(row.harnessId);
}}
onEscape={() => navigate("/" + breadcrumb.slice(0, -1).join("/"))}
onPrevPage={paging.pageIndex > 0 ? paging.prev : undefined}
onNextPage={nextToken ? () => paging.next(nextToken) : undefined}
/>
{paginated && (
<Text color={darkTheme.colors.muted} dimColor>
page {paging.pageIndex + 1}
{nextToken ? " · more →" : ""}
</Text>
)}
</>
)}
</Layout>
getValue={(row) => row.harnessId}
onSelect={onSelect}
onBack={goBack}
loadingMessage="Loading harnesses…"
errorMessage={(error) => `Error: ${error.message}`}
emptyMessage="No harnesses found."
emptyPageMessage="No harnesses on this page."
/>
);
}
Loading
Loading