diff --git a/src/components/Root.tsx b/src/components/Root.tsx
index 4f5c60a9e..9f928022e 100644
--- a/src/components/Root.tsx
+++ b/src/components/Root.tsx
@@ -23,7 +23,10 @@ import { RuntimeScreen } from "../handlers/runtime/screen.tsx";
import { RuntimeGetJsonScreen, RuntimeGetScreen } from "../handlers/runtime/get/screen.tsx";
import { RuntimeListScreen } from "../handlers/runtime/list/screen.tsx";
import { RuntimeEndpointScreen } from "../handlers/runtime/endpoint/screen.tsx";
-import { RuntimeGetEndpointScreen } from "../handlers/runtime/endpoint/get/screen.tsx";
+import {
+ RuntimeGetEndpointJsonScreen,
+ RuntimeGetEndpointScreen,
+} from "../handlers/runtime/endpoint/get/screen.tsx";
import { RuntimeListEndpointsScreen } from "../handlers/runtime/endpoint/list/screen.tsx";
import { RuntimeVersionScreen } from "../handlers/runtime/version/screen.tsx";
import { RuntimeGetVersionScreen } from "../handlers/runtime/version/get/screen.tsx";
@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={}
/>
+ }
+ />
}
diff --git a/src/handlers/runtime/endpoint/endpoint.screen.test.tsx b/src/handlers/runtime/endpoint/endpoint.screen.test.tsx
index 2cdc167ce..5c821472f 100644
--- a/src/handlers/runtime/endpoint/endpoint.screen.test.tsx
+++ b/src/handlers/runtime/endpoint/endpoint.screen.test.tsx
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
import type {
AgentRuntime,
AgentRuntimeEndpoint,
+ GetAgentRuntimeResponse,
GetAgentRuntimeEndpointResponse,
} from "@aws-sdk/client-bedrock-agentcore-control";
import {
@@ -65,6 +66,25 @@ function getEndpointResponse(
};
}
+function getRuntimeResponse(): GetAgentRuntimeResponse {
+ return {
+ agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123",
+ agentRuntimeId: "runtime-123",
+ agentRuntimeName: "checkout",
+ agentRuntimeVersion: "3",
+ createdAt: new Date("2026-07-19T01:02:03.000Z"),
+ lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"),
+ roleArn: "arn:aws:iam::123456789012:role/runtime-role",
+ networkConfiguration: { networkMode: "PUBLIC" },
+ lifecycleConfiguration: {
+ idleRuntimeSessionTimeout: 900,
+ maxLifetime: 28_800,
+ },
+ status: "READY",
+ protocolConfiguration: { serverProtocol: "HTTP" },
+ };
+}
+
async function waitForRuntimePicker(lastFrame: () => string | undefined): Promise {
await waitFor(() => {
const frame = lastFrame() ?? "";
@@ -187,7 +207,7 @@ describe("Runtime endpoint flow", () => {
expect(r.lastFrame()).toContain("endpoint access denied");
});
- test("selecting an encoded qualifier opens complete endpoint JSON with exact selectors", async () => {
+ test("selecting an encoded qualifier opens its summary with exact selectors", async () => {
const qualifier = "prod/blue one";
const core = new TestCoreClient();
core.runtime.setListEndpointsResponse({
@@ -205,7 +225,10 @@ describe("Runtime endpoint flow", () => {
r.lastFrame,
`agentcore → runtime → endpoint → get → runtime-123 → ${qualifier}`,
);
- await waitForText(r.lastFrame, '"targetVersion"');
+ await waitForText(r.lastFrame, "invoke this Runtime endpoint");
+ expect(r.lastFrame()).toMatch(/liveVersion\s+3/);
+ expect(r.lastFrame()).toMatch(/targetVersion\s+4/);
+ expect(r.lastFrame()).toContain("show the full JSON definition");
await waitFor(() => core.runtime.calls.some((call) => call.method === "getRuntimeEndpoint"));
expect(core.runtime.calls.find((call) => call.method === "getRuntimeEndpoint")).toEqual({
method: "getRuntimeEndpoint",
@@ -220,6 +243,79 @@ describe("Runtime endpoint flow", () => {
});
});
+ test("shows the endpoint failure reason only when the service provides one", async () => {
+ const healthyCore = new TestCoreClient();
+ healthyCore.runtime.setGetEndpointResponse(getEndpointResponse());
+ const healthy = renderScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", {
+ core: healthyCore,
+ });
+
+ await waitForText(healthy.lastFrame, "invoke this Runtime endpoint");
+ expect(healthy.lastFrame()).not.toContain("failureReason");
+ healthy.unmount();
+
+ const failedCore = new TestCoreClient();
+ failedCore.runtime.setGetEndpointResponse(
+ getEndpointResponse({
+ status: "UPDATE_FAILED",
+ failureReason: "Endpoint failed its health check",
+ }),
+ );
+ const failed = renderScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", {
+ core: failedCore,
+ });
+
+ await waitForText(failed.lastFrame, "Endpoint failed its health check");
+ expect(failed.lastFrame()).toMatch(/failureReason\s+Endpoint failed its health check/);
+ });
+
+ test("opens complete endpoint JSON from the detail action and returns to the summary", async () => {
+ const core = new TestCoreClient();
+ core.runtime.setGetEndpointResponse(getEndpointResponse());
+ const r = renderScreen("/agentcore/runtime/endpoint/get/runtime-123/prod", { core });
+
+ await waitForText(r.lastFrame, "invoke this Runtime endpoint");
+ await r.press("down");
+ await r.press("return");
+ await waitForText(
+ r.lastFrame,
+ "agentcore → runtime → endpoint → get → runtime-123 → prod → json",
+ );
+ await waitForText(r.lastFrame, '"targetVersion"');
+
+ await r.press("escape");
+ await waitFor(() => {
+ const frame = r.lastFrame() ?? "";
+ return (
+ frame.includes("agentcore → runtime → endpoint → get → runtime-123 → prod") &&
+ !frame.includes("→ json")
+ );
+ });
+ await waitForText(r.lastFrame, "invoke this Runtime endpoint");
+ });
+
+ test("invokes the selected endpoint directly and Esc returns to its summary", async () => {
+ const core = new TestCoreClient();
+ core.runtime
+ .setListEndpointsResponse({ runtimeEndpoints: [endpoint()] })
+ .setGetEndpointResponse(getEndpointResponse())
+ .setGetResponse(getRuntimeResponse());
+ const r = renderScreen("/agentcore/runtime/endpoint/list/runtime-123", { core });
+
+ await waitForText(r.lastFrame, "prod");
+ await r.press("return");
+ await waitForText(r.lastFrame, "invoke this Runtime endpoint");
+ await r.press("return");
+ await waitForText(r.lastFrame, "agentcore → runtime → invoke → runtime-123 → prod");
+ await waitForText(r.lastFrame, "Enter JSON payload");
+
+ await r.press("escape");
+ await waitForText(r.lastFrame, "agentcore → runtime → endpoint → get → runtime-123 → prod");
+ await waitForText(r.lastFrame, "invoke this Runtime endpoint");
+ await r.press("escape");
+ await waitForText(r.lastFrame, "agentcore → runtime → endpoint → list → runtime-123");
+ });
+
test("uses a structural parent for the Runtime picker and history below it", async () => {
const parentCore = new TestCoreClient();
parentCore.runtime.setListResponse({
@@ -254,7 +350,7 @@ describe("Runtime endpoint flow", () => {
await list.press("return");
await waitForText(list.lastFrame, "prod");
await list.press("return");
- await waitForText(list.lastFrame, '"agentRuntimeEndpointArn"');
+ await waitForText(list.lastFrame, "invoke this Runtime endpoint");
await list.press("escape");
await waitForText(list.lastFrame, "agentcore → runtime → endpoint → list → runtime-123");
});
diff --git a/src/handlers/runtime/endpoint/get/screen.tsx b/src/handlers/runtime/endpoint/get/screen.tsx
index c570650b7..5dacae012 100644
--- a/src/handlers/runtime/endpoint/get/screen.tsx
+++ b/src/handlers/runtime/endpoint/get/screen.tsx
@@ -1,23 +1,92 @@
import { useQuery } from "@tanstack/react-query";
-import { useParams } from "react-router";
+import { useNavigate, useParams } from "react-router";
import { JsonDetail } from "../../../../components/JsonDetail";
+import { ResourceDetailScreen } from "../../../../components/ResourceDetailScreen";
import type { ScreenProps } from "../../../types";
import { coreOptsFromCtx } from "../../../utils";
-export function RuntimeGetEndpointScreen({ ctx, core }: ScreenProps) {
+function useRuntimeEndpointDetail(
+ { ctx, core }: ScreenProps,
+ runtimeId: string | undefined,
+ qualifier: string | undefined,
+) {
const opts = coreOptsFromCtx(ctx);
- const { runtimeId, qualifier } = useParams();
- const detail = useQuery({
+ return useQuery({
queryKey: ["runtime-endpoint", opts.region, runtimeId, qualifier],
queryFn: () => core.runtime.getRuntimeEndpoint(runtimeId!, qualifier!, opts),
enabled: runtimeId !== undefined && qualifier !== undefined,
});
+}
+
+function endpointPath(runtimeId: string, qualifier: string, suffix?: string): string {
+ const path = `/agentcore/runtime/endpoint/get/${encodeURIComponent(runtimeId)}/${encodeURIComponent(qualifier)}`;
+ return suffix ? `${path}/${suffix}` : path;
+}
+
+export function RuntimeGetEndpointScreen(props: ScreenProps) {
+ const navigate = useNavigate();
+ const { runtimeId, qualifier } = useParams();
+ const detail = useRuntimeEndpointDetail(props, runtimeId, qualifier);
+ const endpoint = detail.data;
return (
-
+ navigate(
+ `/agentcore/runtime/invoke/${encodeURIComponent(runtimeId)}/${encodeURIComponent(qualifier)}`,
+ { state: { returnOnEscape: true } },
+ ),
+ },
+ {
+ name: "detail",
+ description: "show the full JSON definition",
+ onSelect: () => navigate(endpointPath(runtimeId, qualifier, "json")),
+ },
+ ]
+ : []
+ }
+ loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
+ onRetry={() => void detail.refetch()}
+ />
+ );
+}
+
+export function RuntimeGetEndpointJsonScreen(props: ScreenProps) {
+ const { runtimeId, qualifier } = useParams();
+ const detail = useRuntimeEndpointDetail(props, runtimeId, qualifier);
+
+ return (
+ void detail.refetch()}
diff --git a/src/handlers/runtime/get/screen.tsx b/src/handlers/runtime/get/screen.tsx
index c33528ec1..a144e28ee 100644
--- a/src/handlers/runtime/get/screen.tsx
+++ b/src/handlers/runtime/get/screen.tsx
@@ -7,9 +7,15 @@ import { coreOptsFromCtx } from "../../utils";
const ACTIONS = [
{
- name: "detail",
- description: "show the full JSON definition",
- to: (id: string) => `/agentcore/runtime/get/${encodeURIComponent(id)}/json`,
+ name: "invoke",
+ description: "invoke this Runtime",
+ to: (id: string) => `/agentcore/runtime/invoke/${encodeURIComponent(id)}`,
+ returnsToDetails: true,
+ },
+ {
+ name: "endpoints",
+ description: "list this Runtime's endpoints",
+ to: (id: string) => `/agentcore/runtime/endpoint/list/${encodeURIComponent(id)}`,
},
{
name: "versions",
@@ -17,9 +23,9 @@ const ACTIONS = [
to: (id: string) => `/agentcore/runtime/version/list/${encodeURIComponent(id)}`,
},
{
- name: "endpoints",
- description: "list this Runtime's endpoints",
- to: (id: string) => `/agentcore/runtime/endpoint/list/${encodeURIComponent(id)}`,
+ name: "detail",
+ description: "show the full JSON definition",
+ to: (id: string) => `/agentcore/runtime/get/${encodeURIComponent(id)}/json`,
},
] as const;
@@ -56,7 +62,13 @@ export function RuntimeGetScreen(props: ScreenProps) {
? ACTIONS.map((action) => ({
name: action.name,
description: action.description,
- onSelect: () => navigate(action.to(runtimeId)),
+ onSelect: () =>
+ navigate(action.to(runtimeId), {
+ state:
+ "returnsToDetails" in action && action.returnsToDetails
+ ? { returnOnEscape: true }
+ : undefined,
+ }),
}))
: []
}
diff --git a/src/handlers/runtime/invoke/screen.tsx b/src/handlers/runtime/invoke/screen.tsx
index d428ddb4f..eab299d8c 100644
--- a/src/handlers/runtime/invoke/screen.tsx
+++ b/src/handlers/runtime/invoke/screen.tsx
@@ -3,7 +3,7 @@ import { ServiceException } from "@smithy/core/client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Box, Text, useInput, useWindowSize } from "ink";
import { useQuery } from "@tanstack/react-query";
-import { useNavigate, useParams } from "react-router";
+import { useLocation, useNavigate, useParams } from "react-router";
import { ScrollView, type ScrollViewRef } from "ink-scroll-view";
import cliTruncate from "cli-truncate";
import type { ScreenProps } from "../../types";
@@ -26,6 +26,10 @@ type ExchangeState = "connecting" | "streaming" | "complete" | "interrupted" | "
type TargetPickerState = { stage: "runtime" } | { stage: "endpoint"; runtimeId: string };
+type RuntimeInvokeLocationState = {
+ returnOnEscape?: boolean;
+};
+
type ErrorDetails = {
name: string;
message?: string;
@@ -90,9 +94,11 @@ function ErrorBlock({ details }: { details: ErrorDetails }) {
export function RuntimeInvokeScreen(props: ScreenProps) {
const { runtimeId, qualifier } = useParams();
+ const location = useLocation();
const navigate = useNavigate();
const launchContext = props.ctx.value(RuntimeInvokeLaunchContextKey);
const initialContext = launchContext?.runtimeId === runtimeId ? launchContext : undefined;
+ const returnOnEscape = (location.state as RuntimeInvokeLocationState | null)?.returnOnEscape;
if (!runtimeId) {
return (
@@ -112,8 +118,13 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
breadcrumb={["agentcore", "runtime", "invoke", runtimeId]}
description="choose an endpoint to invoke"
- onSelect={(selected) => navigate(invokePath(runtimeId, selected))}
- onEscape={() => navigate(invokePath())}
+ onSelect={(selected) =>
+ navigate(invokePath(runtimeId, selected), {
+ replace: returnOnEscape === true,
+ state: returnOnEscape ? { returnOnEscape } : undefined,
+ })
+ }
+ onEscape={() => (returnOnEscape ? navigate(-1) : navigate(invokePath()))}
/>
);
}
@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
+ returnOnEscape={returnOnEscape}
/>
);
}
@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
+ returnOnEscape?: boolean;
};
function RuntimeInvokeConsole({
@@ -140,8 +153,10 @@ function RuntimeInvokeConsole({
runtimeId,
qualifier,
initialContext,
+ returnOnEscape,
}: RuntimeInvokeConsoleProps) {
const opts = coreOptsFromCtx(ctx);
+ const navigate = useNavigate();
const { columns, rows } = useWindowSize();
const [target, setTarget] = useState({ runtimeId, qualifier });
const [targetPicker, setTargetPicker] = useState(null);
@@ -292,6 +307,7 @@ function RuntimeInvokeConsole({
}
if (key.escape) {
if (abortRef.current) abortRef.current.abort();
+ else if (returnOnEscape) navigate(-1);
else setTargetPicker({ stage: "endpoint", runtimeId: target.runtimeId });
return;
}
diff --git a/src/handlers/runtime/runtime.screen.test.tsx b/src/handlers/runtime/runtime.screen.test.tsx
index afaa84786..dff9eb0f5 100644
--- a/src/handlers/runtime/runtime.screen.test.tsx
+++ b/src/handlers/runtime/runtime.screen.test.tsx
@@ -208,16 +208,17 @@ describe("runtime hub", () => {
expect(failed.lastFrame()).toMatch(/failureReason\s+Image could not be pulled/);
});
- test("renders exactly the read-only detail, versions, and endpoints actions", async () => {
+ test("renders invoke first with endpoint, version, and detail actions", async () => {
const core = new TestCoreClient();
core.runtime.setGetResponse(getRuntimeResponse());
const r = renderScreen("/agentcore/runtime/get/runtime-123", { core });
await waitForText(r.lastFrame, "show the full JSON definition");
const frame = r.lastFrame()!;
+ expect(frame).toMatch(/❯ invoke\s+invoke this Runtime/);
expect(frame).toContain("versions");
expect(frame).toContain("endpoints");
- for (const excluded of ["invoke", "exec", "update", "create", "delete"]) {
+ for (const excluded of ["exec", "update", "create", "delete"]) {
expect(frame).not.toContain(excluded);
}
});
@@ -244,8 +245,8 @@ describe("runtime hub", () => {
});
test.each([
- ["versions", 1],
- ["endpoints", 2],
+ ["endpoints", 1],
+ ["versions", 2],
] as const)(
"selecting %s opens its encoded Runtime-scoped route",
async (action, downPresses) => {
@@ -303,6 +304,7 @@ describe("runtime hub", () => {
const r = renderScreen("/agentcore/runtime/get/runtime-123", { core });
await waitForText(r.lastFrame, "show the full JSON definition");
+ for (let index = 0; index < 3; index += 1) await r.press("down");
await r.press("return");
await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123 → json");
const frame = r.lastFrame()!;
@@ -399,6 +401,7 @@ describe("runtime hub", () => {
await waitForText(r.lastFrame, "checkout");
await r.press("return");
await waitForText(r.lastFrame, "show the full JSON definition");
+ for (let index = 0; index < 3; index += 1) await r.press("down");
await r.press("return");
await waitForText(r.lastFrame, '"agentRuntimeId"');
await r.press("escape");
@@ -409,6 +412,45 @@ describe("runtime hub", () => {
await waitForText(r.lastFrame, "show the full JSON definition");
});
+ test("invoke keeps the selected Runtime and Esc returns from endpoint selection", async () => {
+ const core = coreWithRuntimes([runtime({ agentRuntimeId: "runtime-123" })]);
+ core.runtime.setGetResponse(getRuntimeResponse());
+ core.runtime.setListEndpointsResponse({
+ runtimeEndpoints: [
+ {
+ name: "prod",
+ id: "prod",
+ liveVersion: "7",
+ agentRuntimeEndpointArn: "arn:endpoint",
+ agentRuntimeArn: "arn:runtime",
+ status: "READY",
+ createdAt: new Date("2026-07-19T01:02:03.000Z"),
+ lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"),
+ },
+ ],
+ });
+ const r = renderScreen("/agentcore/runtime/list", { core });
+
+ await waitForText(r.lastFrame, "checkout");
+ await r.press("return");
+ await waitForText(r.lastFrame, "invoke this Runtime");
+ const listCallsBeforeInvoke = core.runtime.calls.filter(
+ (call) => call.method === "listRuntimes",
+ ).length;
+ await r.press("return");
+ await waitForText(r.lastFrame, "choose an endpoint to invoke");
+ await waitForText(r.lastFrame, "prod");
+ expect(core.runtime.calls.filter((call) => call.method === "listRuntimes")).toHaveLength(
+ listCallsBeforeInvoke,
+ );
+
+ await r.press("escape");
+ await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123");
+ await waitForText(r.lastFrame, "invoke this Runtime");
+ await r.press("escape");
+ await waitForText(r.lastFrame, "agentcore → runtime → list");
+ });
+
test("Esc remains active while the hub is loading", async () => {
const hubCore = coreWithRuntimes([runtime({ agentRuntimeId: "runtime-123" })]);
const hubPending = Promise.withResolvers();