Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion src/components/Root.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -283,6 +286,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier"
element={<RuntimeGetEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/get/:runtimeId/:qualifier/json"
element={<RuntimeGetEndpointJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/endpoint/list"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
Expand Down
102 changes: 99 additions & 3 deletions src/handlers/runtime/endpoint/endpoint.screen.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand DownExpand Up@@ -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<void> {
await waitFor(() => {
const frame = lastFrame() ?? "";
Expand DownExpand Up@@ -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({
Expand All@@ -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",
Expand All@@ -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({
Expand DownExpand Up@@ -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");
});
Expand Down
79 changes: 74 additions & 5 deletions src/handlers/runtime/endpoint/get/screen.tsx
Original file line numberDiff line numberDiff line change
@@ -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 (
<JsonDetail
<ResourceDetailScreen
breadcrumb={["agentcore", "runtime", "endpoint", "get", runtimeId ?? "", qualifier ?? ""]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
items={{
qualifier: endpoint?.name ?? qualifier ?? "",
status: endpoint?.status ?? "",
...(endpoint?.failureReason ? { failureReason: endpoint.failureReason } : {}),
liveVersion: endpoint?.liveVersion ?? "-",
targetVersion: endpoint?.targetVersion ?? "-",
description: endpoint?.description ?? "-",
updatedAt: endpoint?.lastUpdatedAt?.toISOString() ?? "-",
arn: endpoint?.agentRuntimeEndpointArn ?? "",
}}
actions={
runtimeId && qualifier && endpoint
? [
{
name: "invoke",
description: "invoke this Runtime endpoint",
onSelect: () =>
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 (
<JsonDetail
breadcrumb={[
"agentcore",
"runtime",
"endpoint",
"get",
runtimeId ?? "",
qualifier ?? "",
"json",
]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data}
loadingLabel={`Loading endpoint ${qualifier ?? ""} for Runtime ${runtimeId ?? ""}…`}
onRetry={() => void detail.refetch()}
Expand Down
26 changes: 19 additions & 7 deletions src/handlers/runtime/get/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,19 +7,25 @@ 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",
description: "list immutable Runtime versions",
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;

Expand DownExpand Up@@ -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,
}),
}))
: []
}
Expand Down
22 changes: 19 additions & 3 deletions src/handlers/runtime/invoke/screen.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand All@@ -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;
Expand DownExpand Up@@ -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 (
Expand All@@ -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()))}
/>
);
}
Expand All@@ -124,6 +135,7 @@ export function RuntimeInvokeScreen(props: ScreenProps) {
runtimeId={runtimeId}
qualifier={qualifier}
initialContext={initialContext}
returnOnEscape={returnOnEscape}
/>
);
}
Expand All@@ -132,6 +144,7 @@ type RuntimeInvokeConsoleProps = ScreenProps & {
runtimeId: string;
qualifier: string;
initialContext?: RuntimeInvokeLaunchContext;
returnOnEscape?: boolean;
};

function RuntimeInvokeConsole({
Expand All@@ -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<TargetPickerState | null>(null);
Expand DownExpand Up@@ -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;
}
Expand Down
Loading
Loading