diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx
index b6b7c91eb..9e825ea1e 100644
--- a/src/components/CliOnlyScreen.test.tsx
+++ b/src/components/CliOnlyScreen.test.tsx
@@ -46,17 +46,17 @@ describe("menus list command-line-only subcommands below a divider", () => {
const r = renderScreen("/agentcore/eval");
await waitForText(r.lastFrame, "command line only");
- expect(menuEntries(r.lastFrame()!).cliOnly).toEqual(["ondemand", "recommendation"]);
+ expect(menuEntries(r.lastFrame()!).cliOnly).toEqual(["ondemand"]);
r.unmount();
});
test("a menu whose every subcommand is command line only", async () => {
- const r = renderScreen("/agentcore/eval/recommendation");
+ const r = renderScreen("/agentcore/eval/ondemand");
await waitForText(r.lastFrame, "command line only");
expect(menuEntries(r.lastFrame()!)).toEqual({
screens: [],
- cliOnly: ["start", "get", "list", "delete"],
+ cliOnly: ["evaluate", "simulate"],
});
r.unmount();
});
diff --git a/src/components/Root.tsx b/src/components/Root.tsx
index f4cdefa0a..439056ee7 100644
--- a/src/components/Root.tsx
+++ b/src/components/Root.tsx
@@ -58,6 +58,9 @@ import {
import { BatchEvaluationScreen } from "../handlers/eval/batch-evaluation/screen.tsx";
import { BatchEvaluationListScreen } from "../handlers/eval/batch-evaluation/list/screen.tsx";
import { BatchEvaluationGetJsonScreen } from "../handlers/eval/batch-evaluation/get/screen.tsx";
+import { RecommendationScreen } from "../handlers/eval/recommendation/screen.tsx";
+import { RecommendationListScreen } from "../handlers/eval/recommendation/list/screen.tsx";
+import { RecommendationGetJsonScreen } from "../handlers/eval/recommendation/get/screen.tsx";
import { BatchInsightsScreen } from "../handlers/eval/batch-insights/screen.tsx";
import { BatchInsightsListScreen } from "../handlers/eval/batch-insights/list/screen.tsx";
import { BatchInsightsGetJsonScreen } from "../handlers/eval/batch-insights/get/screen.tsx";
@@ -618,6 +621,24 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/eval/batch-evaluation/get/:batchEvaluationId"
element={}
/>
+ }
+ />
+ }
+ />
+ {/* Bare `get` (no id) has nothing to show — send the user to the list. */}
+ }
+ />
+ {/* get is raw JSON only — no metadata hub, so :id is the JSON view. */}
+ }
+ />
} />
.renderJson(await core.eval.getRecommendation(flags["id"], coreOptsFromCtx(ctx)));
},
});
+
+export { RecommendationGetJsonScreen } from "./screen.tsx";
diff --git a/src/handlers/eval/recommendation/get/screen.tsx b/src/handlers/eval/recommendation/get/screen.tsx
new file mode 100644
index 000000000..70e39941a
--- /dev/null
+++ b/src/handlers/eval/recommendation/get/screen.tsx
@@ -0,0 +1,32 @@
+import { useQuery } from "@tanstack/react-query";
+import { useParams } from "react-router";
+import { JsonDetail } from "../../../../components/JsonDetail";
+import type { ScreenProps } from "../../../types";
+import { coreOptsFromCtx } from "../../../utils";
+
+function useRecommendationDetail({ ctx, core }: ScreenProps, id: string | undefined) {
+ const opts = coreOptsFromCtx(ctx);
+ return useQuery({
+ queryKey: ["recommendation", opts.region, id],
+ queryFn: () => core.eval.getRecommendation(id!, opts),
+ enabled: id !== undefined,
+ });
+}
+
+// Recommendation get is raw JSON only — no metadata hub, matching batch-evaluation.
+// The full response is the value; a curated field subset would just hide data.
+export function RecommendationGetJsonScreen(props: ScreenProps) {
+ const { recommendationId } = useParams();
+ const query = useRecommendationDetail(props, recommendationId);
+
+ return (
+ void query.refetch()}
+ />
+ );
+}
diff --git a/src/handlers/eval/recommendation/index.tsx b/src/handlers/eval/recommendation/index.tsx
index 917664075..5e129242c 100644
--- a/src/handlers/eval/recommendation/index.tsx
+++ b/src/handlers/eval/recommendation/index.tsx
@@ -1,16 +1,24 @@
import type { AppIO } from "../../../io";
import { Router } from "../../../router";
+import { renderTui } from "../../../tui";
+import { withTuiOnEmptyFlagsAndArgs } from "../../../middleware";
import type { Core } from "../../types";
import { createDeleteRecommendationHandler } from "./delete";
import { createGetRecommendationHandler } from "./get";
import { createListRecommendationsHandler } from "./list";
import { createStartRecommendationHandler } from "./start";
+// A bare invocation opens the interactive TUI (list → get), matching
+// batch-evaluation; start/delete stay below the command-line-only divider.
export function createRecommendationHandler(core: Core, io: AppIO): Router {
return new Router("recommendation", "manage AgentCore recommendations")
- .supportedTuiCommands()
+ .use(withTuiOnEmptyFlagsAndArgs(core, io))
+ .default(renderTui(core, io))
+ .supportedTuiCommands("get", "list")
.handler(createStartRecommendationHandler(core, io))
.handler(createGetRecommendationHandler(core))
.handler(createListRecommendationsHandler(core))
.handler(createDeleteRecommendationHandler(core));
}
+
+export { RecommendationScreen } from "./screen.tsx";
diff --git a/src/handlers/eval/recommendation/list/index.tsx b/src/handlers/eval/recommendation/list/index.tsx
index 3f2a86386..1fdc80cd6 100644
--- a/src/handlers/eval/recommendation/list/index.tsx
+++ b/src/handlers/eval/recommendation/list/index.tsx
@@ -35,3 +35,5 @@ export const createListRecommendationsHandler = (core: Core) =>
ctx.require(JsonRendererKey).renderJson(response);
},
});
+
+export { RecommendationListScreen } from "./screen.tsx";
diff --git a/src/handlers/eval/recommendation/list/screen.tsx b/src/handlers/eval/recommendation/list/screen.tsx
new file mode 100644
index 000000000..ac2b2b736
--- /dev/null
+++ b/src/handlers/eval/recommendation/list/screen.tsx
@@ -0,0 +1,74 @@
+import type { RecommendationSummary } from "@aws-sdk/client-bedrock-agentcore";
+import { useNavigate } from "react-router";
+import { formatTimestamp } from "../../../../components/formatTimestamp";
+import { PaginatedTablePicker } from "../../../../components/PaginatedTablePicker";
+import type { DataTableColumn } from "../../../../components/ui/data-table";
+import type { ScreenProps } from "../../../types";
+import { coreOptsFromCtx } from "../../../utils";
+
+// RecommendationRow is the flat, display-ready shape the table renders. It also
+// satisfies DataTable's `T extends Record` constraint, which the
+// SDK's RecommendationSummary interface does not.
+interface RecommendationRow extends Record {
+ recommendationId: string;
+ name: string;
+ type: string;
+ status: string;
+ updatedAt: string;
+}
+
+// Recommendation types are the verbose enum values SYSTEM_PROMPT_RECOMMENDATION /
+// TOOL_DESCRIPTION_RECOMMENDATION; the shared `_RECOMMENDATION` suffix is noise
+// that would otherwise force truncation, so drop it for display.
+const columns = [
+ { key: "name", header: "name", flex: true },
+ {
+ key: "type",
+ header: "type",
+ width: 16,
+ render: (v) => String(v).replace(/_RECOMMENDATION$/, ""),
+ },
+ { key: "status", header: "status", width: 14 },
+ { key: "updatedAt", header: "updated UTC", width: 16, render: formatTimestamp },
+] satisfies DataTableColumn[];
+
+function toRow(summary: RecommendationSummary): RecommendationRow {
+ const id = summary.recommendationId ?? "";
+ return {
+ recommendationId: id,
+ name: summary.name ?? id,
+ type: summary.type ?? "-",
+ status: summary.status ?? "-",
+ updatedAt: summary.updatedAt?.toISOString() ?? "-",
+ };
+}
+
+export function RecommendationListScreen({ ctx, core }: ScreenProps) {
+ const opts = coreOptsFromCtx(ctx);
+ const navigate = useNavigate();
+ const breadcrumb = ["agentcore", "eval", "recommendation", "list"];
+
+ return (
+ {
+ const response = await core.eval.listRecommendations(token, pageSize, undefined, opts);
+ return {
+ items: response.recommendationSummaries ?? [],
+ nextToken: response.nextToken,
+ };
+ }}
+ toRow={toRow}
+ columns={columns}
+ getValue={(row) => row.recommendationId}
+ onSelect={(id) => navigate(`/agentcore/eval/recommendation/get/${encodeURIComponent(id)}`)}
+ onBack={() => navigate("/" + breadcrumb.slice(0, -1).join("/"))}
+ loadingMessage="loading recommendations…"
+ errorMessage={(error) => `Error: ${error.message}`}
+ emptyMessage="No recommendations found in this Region."
+ emptyPageMessage="No recommendations on this page."
+ />
+ );
+}
diff --git a/src/handlers/eval/recommendation/recommendation.screen.test.tsx b/src/handlers/eval/recommendation/recommendation.screen.test.tsx
new file mode 100644
index 000000000..c427e57c5
--- /dev/null
+++ b/src/handlers/eval/recommendation/recommendation.screen.test.tsx
@@ -0,0 +1,166 @@
+import { afterEach, describe, expect, test } from "bun:test";
+import type {
+ GetRecommendationResponse,
+ RecommendationSummary,
+} from "@aws-sdk/client-bedrock-agentcore";
+import {
+ cleanupScreens,
+ renderScreen,
+ TestCoreClient,
+ waitFor,
+ waitForText,
+} from "../../../testing";
+
+afterEach(cleanupScreens);
+
+const evalEndpointUrl = "https://eval.test";
+
+function summary(overrides: Partial = {}): RecommendationSummary {
+ return {
+ recommendationArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:recommendation/rec-1",
+ recommendationId: "rec-1",
+ name: "prompt_tuning",
+ type: "SYSTEM_PROMPT_RECOMMENDATION",
+ status: "COMPLETED",
+ createdAt: new Date("2026-07-19T01:02:03.000Z"),
+ updatedAt: new Date("2026-07-20T12:34:56.000Z"),
+ ...overrides,
+ };
+}
+
+function coreWith(items: RecommendationSummary[]): TestCoreClient {
+ const core = new TestCoreClient();
+ core.eval.setListRecommendationsResponse({ recommendationSummaries: items });
+ return core;
+}
+
+describe("recommendation menu", () => {
+ test("offers get and list", async () => {
+ const screen = renderScreen("/agentcore/eval/recommendation");
+ await waitForText(screen.lastFrame, "list recommendations");
+ const frame = screen.lastFrame()!;
+ expect(frame).toContain("list");
+ expect(frame).toContain("get");
+ });
+});
+
+describe("recommendation picker", () => {
+ test("renders name, type, status, and updated time", async () => {
+ const core = coreWith([
+ summary({
+ name: "staging_rec",
+ type: "TOOL_DESCRIPTION_RECOMMENDATION",
+ status: "FAILED",
+ updatedAt: new Date("2026-07-21T02:03:04.000Z"),
+ }),
+ ]);
+ const screen = renderScreen("/agentcore/eval/recommendation/list", { core });
+
+ await waitForText(screen.lastFrame, "staging_rec");
+ const frame = screen.lastFrame()!;
+ // The `_RECOMMENDATION` suffix is stripped so the verbose enum fits the column.
+ expect(frame).toContain("TOOL_DESCRIPTION");
+ expect(frame).not.toContain("TOOL_DESCRIPTION_RECOMMENDATION");
+ expect(frame).toContain("FAILED");
+ expect(frame).toContain("2026-07-21 02:03");
+ });
+
+ test("calls listRecommendations with exact Core options and no status filter", async () => {
+ const core = coreWith([summary()]);
+ renderScreen("/agentcore/eval/recommendation/list", { core, endpointUrl: evalEndpointUrl });
+
+ await waitFor(() => core.eval.calls.some((c) => c.method === "listRecommendations"));
+ expect(core.eval.calls.filter((c) => c.method === "listRecommendations")).toEqual([
+ {
+ method: "listRecommendations",
+ args: [
+ undefined,
+ expect.any(Number),
+ undefined,
+ { region: "us-east-1", endpointUrl: evalEndpointUrl },
+ ],
+ },
+ ]);
+ });
+
+ test("falls back to id and dashes when summary fields are missing", async () => {
+ const core = coreWith([
+ // Only an id — every other display field absent.
+ { recommendationId: "rec-bare" } as RecommendationSummary,
+ ]);
+ const screen = renderScreen("/agentcore/eval/recommendation/list", { core });
+
+ await waitForText(screen.lastFrame, "rec-bare");
+ expect(screen.lastFrame()).toContain("-");
+ });
+
+ test("bare get redirects to the picker", async () => {
+ const core = coreWith([summary({ recommendationId: "redirected", name: "redirected_rec" })]);
+ const screen = renderScreen("/agentcore/eval/recommendation/get", { core });
+
+ await waitForText(screen.lastFrame, "redirected_rec");
+ expect(core.eval.calls[0]?.method).toBe("listRecommendations");
+ });
+
+ test("selection opens the matching recommendation JSON", async () => {
+ const core = coreWith([summary({ recommendationId: "rec-1" })]);
+ core.eval.setGetRecommendationResponse({
+ recommendationId: "rec-1",
+ name: "prompt_tuning",
+ } as GetRecommendationResponse);
+ const screen = renderScreen("/agentcore/eval/recommendation/list", { core });
+
+ await waitForText(screen.lastFrame, "prompt_tuning");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "agentcore → eval → recommendation → get → rec-1");
+ await waitFor(() =>
+ core.eval.calls.some((c) => c.method === "getRecommendation" && c.args[0] === "rec-1"),
+ );
+ });
+
+ test("shows the empty state", async () => {
+ const empty = renderScreen("/agentcore/eval/recommendation/list");
+ await waitForText(empty.lastFrame, "No recommendations found in this Region.");
+ });
+});
+
+describe("recommendation detail (raw JSON)", () => {
+ test("renders the full response", async () => {
+ const core = new TestCoreClient();
+ core.eval.setGetRecommendationResponse({
+ recommendationId: "rec-1",
+ name: "prompt_tuning",
+ status: "COMPLETED",
+ } as GetRecommendationResponse);
+ const screen = renderScreen("/agentcore/eval/recommendation/get/rec-1", {
+ core,
+ endpointUrl: evalEndpointUrl,
+ });
+
+ await waitForText(screen.lastFrame, "prompt_tuning");
+ const frame = screen.lastFrame()!;
+ expect(frame).toContain('"status"');
+ expect(frame).toContain("COMPLETED");
+ expect(core.eval.calls.find((c) => c.method === "getRecommendation")).toEqual({
+ method: "getRecommendation",
+ args: ["rec-1", { region: "us-east-1", endpointUrl: evalEndpointUrl }],
+ });
+ });
+
+ test("retries a failed detail query", async () => {
+ const core = new TestCoreClient();
+ core.eval.setError(new Error("recommendation unavailable"));
+ const screen = renderScreen("/agentcore/eval/recommendation/get/rec-1", { core });
+
+ await waitForText(screen.lastFrame, "recommendation unavailable");
+ expect(screen.lastFrame()).toContain("[r] retry");
+
+ core.eval.setError(undefined);
+ core.eval.setGetRecommendationResponse({
+ recommendationId: "rec-1",
+ name: "prompt_tuning",
+ } as GetRecommendationResponse);
+ await screen.write("r");
+ await waitForText(screen.lastFrame, "prompt_tuning");
+ });
+});
diff --git a/src/handlers/eval/recommendation/screen.tsx b/src/handlers/eval/recommendation/screen.tsx
new file mode 100644
index 000000000..2db19b4b4
--- /dev/null
+++ b/src/handlers/eval/recommendation/screen.tsx
@@ -0,0 +1,6 @@
+import { RouterScreen } from "../../../components/RouterScreen";
+import type { ScreenProps } from "../../types";
+
+export function RecommendationScreen(props: ScreenProps) {
+ return ;
+}