diff --git a/src/components/Root.tsx b/src/components/Root.tsx
index 2102251ef3..5b345e8da7 100644
--- a/src/components/Root.tsx
+++ b/src/components/Root.tsx
@@ -130,6 +130,7 @@ import { DeployProjectScreen } from "../handlers/project/deploy/screen.tsx";
import { ProjectCreateScreen } from "../handlers/project/create/screen.tsx";
import { ProjectInvokePickerScreen } from "../handlers/project/invoke/screen.tsx";
import { AddRuntimeScreen } from "../handlers/project/add/runtime/screen.tsx";
+import { AddRuntimeEndpointScreen } from "../handlers/project/add/runtime-endpoint/screen.tsx";
import { AddMemoryScreen } from "../handlers/project/add/memory/screen.tsx";
import { AddGatewayScreen } from "../handlers/project/add/gateway/screen.tsx";
import { AddGatewayTargetScreen } from "../handlers/project/add/gateway-target/screen.tsx";
@@ -914,6 +915,10 @@ function RouteTable({ ctx, core }: ScreenProps) {
} />
} />
} />
+ }
+ />
} />
} />
void;
required?: boolean;
schema?: z.ZodType;
+ // number makes validation fail unless the value is all digits, and hands the
+ // schema a Number rather than the string; typing itself is not restricted.
+ number?: boolean;
};
export type MultiTextFieldProps = {
@@ -465,6 +468,7 @@ export function MultiTextField({ inputs }: MultiTextFieldProps) {
label: input.label,
required: input.required ?? false,
schema: input.schema,
+ number: input.number ?? false,
});
useInput((_input, key) => {
diff --git a/src/handlers/project/add/add.screen.test.tsx b/src/handlers/project/add/add.screen.test.tsx
index 7d7c136327..b5a7260056 100644
--- a/src/handlers/project/add/add.screen.test.tsx
+++ b/src/handlers/project/add/add.screen.test.tsx
@@ -23,6 +23,7 @@ function addSubcommands(): string[] {
// "cli" divider and opens its help instead.
const WITH_SCREENS = [
"runtime",
+ "runtime-endpoint",
"memory",
"gateway",
"gateway-target",
diff --git a/src/handlers/project/add/index.ts b/src/handlers/project/add/index.ts
index 3010516a4c..20f6136c29 100644
--- a/src/handlers/project/add/index.ts
+++ b/src/handlers/project/add/index.ts
@@ -29,6 +29,7 @@ export function createAddProjectResourceHandler(
// CliOnlyScreen).
const projectAdd = new Router("add", "add project resources").supportedTuiCommands(
"runtime",
+ "runtime-endpoint",
"memory",
"gateway",
"gateway-target",
diff --git a/src/handlers/project/add/runtime-endpoint/index.ts b/src/handlers/project/add/runtime-endpoint/index.ts
index a5fb5d6b93..110a4fd241 100644
--- a/src/handlers/project/add/runtime-endpoint/index.ts
+++ b/src/handlers/project/add/runtime-endpoint/index.ts
@@ -1,8 +1,38 @@
import z from "zod";
import { createHandler, flag, ProjectKey } from "../../../../router";
+import type { AddResourceInput } from "../../types";
import type { AddProjectResourceConfig } from "../types";
import { addProjectResource } from "../shared";
+// The version an endpoint points to when none is given — what --version
+// documents as its default, and what the wizard prefills.
+export const DEFAULT_ENDPOINT_VERSION = 1;
+
+// RuntimeEndpointInput is the endpoint as the flags state it.
+export type RuntimeEndpointInput = {
+ runtime: string;
+ name: string;
+ version?: number;
+ description?: string;
+};
+
+type AddRuntimeEndpoint = Extract;
+
+// toAddRuntimeEndpointInput is the one place a runtime endpoint is built from
+// user input — the flags, or the wizard's answers — so both default the version
+// and drop an empty description the same way.
+export function toAddRuntimeEndpointInput(input: RuntimeEndpointInput): AddRuntimeEndpoint {
+ return {
+ resourceType: "runtime-endpoint",
+ runtimeName: input.runtime,
+ resourceConfig: {
+ name: input.name,
+ version: input.version ?? DEFAULT_ENDPOINT_VERSION,
+ ...(input.description ? { description: input.description } : {}),
+ },
+ };
+}
+
export const createAddRuntimeEndpointHandler = (config: AddProjectResourceConfig) =>
createHandler({
name: "runtime-endpoint",
@@ -12,29 +42,26 @@ export const createAddRuntimeEndpointHandler = (config: AddProjectResourceConfig
flag("name", "the endpoint name (e.g., prod, staging)", z.string().min(1)),
flag(
"version",
- "the runtime version this endpoint points to (default: 1)",
+ `the runtime version this endpoint points to (default: ${DEFAULT_ENDPOINT_VERSION})`,
z.number().int().min(1).optional(),
),
flag("description", "description of the endpoint", z.string().optional()),
],
handle: async (ctx, flags) => {
const project = ctx.require(ProjectKey);
- const version = flags.version ?? 1;
+ const input = toAddRuntimeEndpointInput({
+ runtime: flags.runtime,
+ name: flags.name,
+ version: flags.version,
+ description: flags.description,
+ });
await addProjectResource(
ctx,
config,
project,
- {
- resourceType: "runtime-endpoint",
- runtimeName: flags.runtime,
- resourceConfig: {
- name: flags.name,
- version,
- ...(flags.description ? { description: flags.description } : {}),
- },
- },
- `added runtime endpoint '${flags.name}' (version ${version}) to runtime '${flags.runtime}' in '${project.name}'`,
+ input,
+ `added runtime endpoint '${flags.name}' (version ${input.resourceConfig.version}) to runtime '${flags.runtime}' in '${project.name}'`,
);
},
});
diff --git a/src/handlers/project/add/runtime-endpoint/runtime-endpoint.screen.test.tsx b/src/handlers/project/add/runtime-endpoint/runtime-endpoint.screen.test.tsx
new file mode 100644
index 0000000000..7cb573193f
--- /dev/null
+++ b/src/handlers/project/add/runtime-endpoint/runtime-endpoint.screen.test.tsx
@@ -0,0 +1,382 @@
+import { afterEach, describe, expect, test } from "bun:test";
+import {
+ cleanupScreens,
+ createSilentLogger,
+ flatFrame,
+ initProject,
+ renderScreen,
+ TestCoreClient,
+ TestGlobalConfigAccessor,
+ testIO,
+ ttyTestIO,
+ waitFor,
+ waitForFlatText,
+ waitForText,
+ type RenderScreenResult,
+} from "../../../../testing";
+import { createRootHandler } from "../../../index";
+import { InputValidationError } from "../../../../errors";
+import type { AppIO } from "../../../../io";
+import { projectSpec, writeProjectSpec } from "../gateway-test-support";
+import { endpointNameSchema } from "./screen";
+
+type Endpoints = Record;
+
+const cleanups: Array<() => Promise> = [];
+afterEach(() => Promise.all(cleanups.splice(0).map((cleanup) => cleanup())));
+afterEach(cleanupScreens);
+
+function buildRoot(io: AppIO) {
+ return createRootHandler(new TestCoreClient(), {
+ io,
+ logger: createSilentLogger(),
+ globalConfigAccessor: new TestGlobalConfigAccessor(),
+ });
+}
+
+async function run(args: string[]) {
+ const io = testIO();
+ await buildRoot(io.io).route(["node", "agentcore", ...args]);
+ return io;
+}
+
+// A minimal runtime is enough for an endpoint's parent; scaffolding a real one
+// would install dependencies the endpoint path never touches.
+function runtimeSpec(name: string, endpoints?: Endpoints) {
+ return {
+ name,
+ build: "CodeZip",
+ entrypoint: "main.py",
+ codeLocation: `app/${name}`,
+ runtimeVersion: "PYTHON_3_13",
+ ...(endpoints ? { endpoints } : {}),
+ };
+}
+
+// withRuntimes starts a project with exactly the given runtimes — none, when
+// called bare — so the first step's list is known.
+async function withRuntimes(...runtimes: ReturnType[]): Promise {
+ const { projectRoot, cleanup } = await initProject({
+ prefix: "agentcore-add-runtime-endpoint-wizard-",
+ });
+ cleanups.push(cleanup);
+ const spec = await projectSpec(projectRoot);
+ await writeProjectSpec(projectRoot, { ...spec, runtimes });
+ return projectRoot;
+}
+
+async function endpointsOf(projectRoot: string, runtime: string): Promise {
+ const spec = await projectSpec(projectRoot);
+ const runtimes = spec.runtimes as Array<{ name: string; endpoints?: Endpoints }>;
+ return runtimes.find((candidate) => candidate.name === runtime)?.endpoints;
+}
+
+// reachDetailsStep confirms the first runtime and names the endpoint, leaving
+// the wizard on the version step with its prefilled 1.
+async function reachDetailsStep(screen: RenderScreenResult, name: string): Promise {
+ await waitForText(screen.lastFrame, "which Runtime should this endpoint belong to?");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "what should this endpoint be called?");
+ await screen.write(name);
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "which Runtime version does it point to?");
+}
+
+describe("runtime-endpoint wizard helpers", () => {
+ test("endpointNameSchema applies the name rule and refuses a name the Runtime already has", () => {
+ const schema = endpointNameSchema(runtimeSpec("agent", { prod: { version: 1 } }) as never);
+ expect(schema.safeParse("staging").success).toBe(true);
+ expect(schema.safeParse("9lives").success).toBe(false);
+ const taken = schema.safeParse("prod");
+ expect(taken.success).toBe(false);
+ expect(taken.error?.issues[0]?.message).toBe(
+ "an endpoint named 'prod' already exists on runtime 'agent'",
+ );
+ // Without a runtime to check against, only the name rule applies.
+ expect(endpointNameSchema(undefined).safeParse("prod").success).toBe(true);
+ });
+});
+
+describe("project add runtime-endpoint wizard", () => {
+ test("adds the same endpoint as the flags, on the Runtime picked from the list", async () => {
+ const projectRoot = await withRuntimes(
+ runtimeSpec("agent", { prod: { version: 1 } }),
+ runtimeSpec("worker"),
+ );
+ const screen = renderScreen("/agentcore/add/runtime-endpoint");
+
+ // Each Runtime shows the endpoints it already has.
+ await waitForText(screen.lastFrame, "which Runtime should this endpoint belong to?");
+ expect(screen.lastFrame()).toContain("❯ ● agent");
+ expect(screen.lastFrame()).toContain("endpoint: prod");
+ expect(screen.lastFrame()).toContain("no endpoints yet");
+ await screen.press("down");
+ await waitForText(screen.lastFrame, "❯ ● worker");
+ await screen.press("return");
+
+ await waitForText(screen.lastFrame, "what should this endpoint be called?");
+ await screen.write("staging");
+ await screen.press("return");
+
+ // Version and description share a step; the version is prefilled with 1.
+ await waitForText(screen.lastFrame, "which Runtime version does it point to?");
+ expect(screen.lastFrame()).toContain("Version");
+ expect(screen.lastFrame()).toContain("Description");
+ await screen.press("backspace");
+ await screen.write("3");
+ await screen.press("return");
+ await screen.write("Production traffic");
+ await screen.press("return");
+
+ await waitForText(screen.lastFrame, "this endpoint will be added to agentcore.json");
+ const review = flatFrame(screen.lastFrame);
+ expect(review).toContain("runtime worker");
+ expect(review).toContain("endpoint staging");
+ expect(review).toContain("version 3");
+ expect(review).toContain("description Production traffic");
+ await screen.press("return");
+
+ await waitForFlatText(
+ screen.lastFrame,
+ "added runtime endpoint 'staging' (version 3) to runtime 'worker' in 'TestProject'",
+ );
+ expect(screen.lastFrame()).toContain("agentcore deploy");
+ expect(await endpointsOf(projectRoot, "worker")).toEqual({
+ staging: { version: 3, description: "Production traffic" },
+ });
+ expect(await endpointsOf(projectRoot, "agent")).toEqual({ prod: { version: 1 } });
+
+ // The flags, given the same answers, write the same endpoint.
+ await run([
+ "add",
+ "runtime-endpoint",
+ "--runtime",
+ "agent",
+ "--name",
+ "staging",
+ "--version",
+ "3",
+ "--description",
+ "Production traffic",
+ ]);
+ expect((await endpointsOf(projectRoot, "agent"))?.staging).toEqual(
+ (await endpointsOf(projectRoot, "worker"))?.staging,
+ );
+ screen.unmount();
+ });
+
+ test("version defaults to 1 and the description is optional", async () => {
+ const projectRoot = await withRuntimes(runtimeSpec("agent"));
+ const screen = renderScreen("/agentcore/add/runtime-endpoint");
+
+ await reachDetailsStep(screen, "staging");
+ await screen.press("return");
+ await screen.press("return");
+
+ await waitForText(screen.lastFrame, "this endpoint will be added to agentcore.json");
+ const review = flatFrame(screen.lastFrame);
+ expect(review).toContain("version 1");
+ expect(review).toContain("description (none)");
+ await screen.press("return");
+
+ await waitForFlatText(
+ screen.lastFrame,
+ "added runtime endpoint 'staging' (version 1) to runtime 'agent' in 'TestProject'",
+ );
+ expect(await endpointsOf(projectRoot, "agent")).toEqual({ staging: { version: 1 } });
+ screen.unmount();
+ });
+
+ test("a name the chosen Runtime already has is refused as it is typed", async () => {
+ await withRuntimes(runtimeSpec("agent", { prod: { version: 2 } }));
+ const screen = renderScreen("/agentcore/add/runtime-endpoint");
+
+ await waitForText(screen.lastFrame, "which Runtime should this endpoint belong to?");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "what should this endpoint be called?");
+ await screen.write("prod");
+ await waitForText(
+ screen.lastFrame,
+ "an endpoint named 'prod' already exists on runtime 'agent'",
+ );
+ await screen.press("return");
+ expect(screen.lastFrame()).toContain("what should this endpoint be called?");
+
+ // Any other well-formed name goes through.
+ await screen.press("backspace");
+ await screen.write("uction");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "which Runtime version does it point to?");
+ screen.unmount();
+ });
+
+ test("the version must be a whole number of at least 1", async () => {
+ await withRuntimes(runtimeSpec("agent"));
+ const screen = renderScreen("/agentcore/add/runtime-endpoint");
+
+ await reachDetailsStep(screen, "staging");
+ await screen.press("backspace");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "Version is required");
+
+ await screen.write("0");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "Version must be 1 or higher");
+
+ await screen.press("backspace");
+ await screen.write("v2");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "Version must be a whole number");
+
+ await screen.press("backspace");
+ await screen.press("backspace");
+ await screen.write("2");
+ await screen.press("return");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "this endpoint will be added to agentcore.json");
+ expect(flatFrame(screen.lastFrame)).toContain("version 2");
+ screen.unmount();
+ });
+
+ test("without a Runtime the first step says what to add and esc returns to the menu", async () => {
+ await withRuntimes();
+ const screen = renderScreen("/agentcore/add/runtime-endpoint");
+
+ await waitForText(screen.lastFrame, "no Runtimes in this project");
+ expect(screen.lastFrame()).toContain("agentcore add runtime");
+
+ await screen.press("escape");
+ await waitForText(screen.lastFrame, "add project resources");
+ screen.unmount();
+ });
+
+ test("a rejected add reports itself and hands the form back", async () => {
+ const projectRoot = await withRuntimes(runtimeSpec("agent"));
+ const screen = renderScreen("/agentcore/add/runtime-endpoint");
+
+ await reachDetailsStep(screen, "prod");
+ await screen.press("return");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "this endpoint will be added to agentcore.json");
+
+ // The name was free when typed; it is taken by the time the review submits.
+ await run([
+ "add",
+ "runtime-endpoint",
+ "--runtime",
+ "agent",
+ "--name",
+ "prod",
+ "--version",
+ "4",
+ ]);
+ await screen.press("return");
+
+ await waitForFlatText(
+ screen.lastFrame,
+ "a runtime-endpoint named 'prod' already exists on runtime 'agent'",
+ );
+ await screen.press("escape");
+ await waitForText(screen.lastFrame, "this endpoint will be added to agentcore.json");
+ expect(flatFrame(screen.lastFrame)).toContain("endpoint prod");
+
+ expect(await endpointsOf(projectRoot, "agent")).toEqual({ prod: { version: 4 } });
+ screen.unmount();
+ });
+
+ test("esc on the first step returns to the add menu", async () => {
+ await withRuntimes(runtimeSpec("agent"));
+ const screen = renderScreen("/agentcore/add/runtime-endpoint");
+
+ await waitForText(screen.lastFrame, "which Runtime should this endpoint belong to?");
+ await screen.press("escape");
+
+ await waitForText(screen.lastFrame, "add project resources");
+ screen.unmount();
+ });
+});
+
+describe("project add runtime-endpoint dispatch", () => {
+ const MISSING_RUNTIME = "required option '--runtime' not specified";
+
+ async function routeError(io: AppIO, args: string[]): Promise {
+ return buildRoot(io)
+ .route(["node", "agentcore", "add", "runtime-endpoint", ...args])
+ .then(
+ () => undefined,
+ (caught: unknown) => caught,
+ );
+ }
+
+ test("bare add runtime-endpoint in a TTY session opens the wizard", async () => {
+ await withRuntimes(runtimeSpec("agent"));
+ const { streams, stdin } = ttyTestIO();
+
+ const outcome = buildRoot(streams.io)
+ .route(["node", "agentcore", "add", "runtime-endpoint"])
+ .then(
+ () => ({ ok: true as const }),
+ (error: unknown) => ({ ok: false as const, error }),
+ );
+ let settled = false;
+ void outcome.finally(() => {
+ settled = true;
+ });
+
+ await waitFor(
+ () => {
+ if (!settled) stdin.write("\x03");
+ return settled;
+ },
+ 5000,
+ 150,
+ );
+ expect(await outcome).toEqual({ ok: true });
+ expect(streams.stderr()).not.toContain("required option");
+ }, 10000);
+
+ test("bare add runtime-endpoint without a TTY stays headless and reports the missing --runtime", async () => {
+ await withRuntimes(runtimeSpec("agent"));
+
+ const error = await routeError(testIO().io, []);
+
+ expect(error).toBeInstanceOf(InputValidationError);
+ expect((error as Error).message).toContain(MISSING_RUNTIME);
+ });
+
+ test("any user-supplied flag stays headless even in a TTY", async () => {
+ await withRuntimes(runtimeSpec("agent"));
+
+ const error = await routeError(ttyTestIO().streams.io, ["--name", "prod"]);
+
+ expect(error).toBeInstanceOf(InputValidationError);
+ expect((error as Error).message).toContain(MISSING_RUNTIME);
+ });
+
+ test("--json stays headless even in a TTY", async () => {
+ await withRuntimes(runtimeSpec("agent"));
+
+ const error = await routeError(ttyTestIO().streams.io, ["--json"]);
+
+ expect(error).toBeInstanceOf(InputValidationError);
+ expect((error as Error).message).toContain(MISSING_RUNTIME);
+ });
+
+ test("flag-driven add runtime-endpoint still runs headless in a TTY session", async () => {
+ const projectRoot = await withRuntimes(runtimeSpec("agent"));
+ const { streams } = ttyTestIO();
+
+ await buildRoot(streams.io).route([
+ "node",
+ "agentcore",
+ "add",
+ "runtime-endpoint",
+ "--runtime",
+ "agent",
+ "--name",
+ "prod",
+ ]);
+
+ expect(await endpointsOf(projectRoot, "agent")).toEqual({ prod: { version: 1 } });
+ });
+});
diff --git a/src/handlers/project/add/runtime-endpoint/screen.tsx b/src/handlers/project/add/runtime-endpoint/screen.tsx
new file mode 100644
index 0000000000..8dd874ddb7
--- /dev/null
+++ b/src/handlers/project/add/runtime-endpoint/screen.tsx
@@ -0,0 +1,205 @@
+import { useState } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { useNavigate } from "react-router";
+import z from "zod";
+import {
+ MultiTextField,
+ ResourceChoiceField,
+ Step,
+ Summary,
+ TextField,
+ Wizard,
+ type Choice,
+} from "../../../../components/wizard";
+import {
+ RuntimeEndpointNameSchema,
+ RuntimeEndpointSchema,
+ type ProjectRuntime,
+} from "../../../../projectSchemas/runtime";
+import { ProjectKey } from "../../../../router";
+import type { ScreenProps } from "../../../types";
+import type { Project } from "../../types";
+import { ProjectGate, projectQueryKey } from "../../ProjectGate";
+import {
+ DEFAULT_ENDPOINT_VERSION,
+ toAddRuntimeEndpointInput,
+ type RuntimeEndpointInput,
+} from "./index";
+
+const BREADCRUMB = ["agentcore", "add", "runtime-endpoint"];
+const DESCRIPTION = "add a named endpoint (version alias) to a runtime";
+const ADD_MENU = "/agentcore/add";
+
+// The version rule the spec applies, with a message that names the input; the
+// whole-number check itself happens before the schema sees the value.
+const VERSION_SCHEMA = z.number().int().min(1, "Version must be 1 or higher");
+
+type RuntimeEndpointFormValues = {
+ runtime: string;
+ name: string;
+ version: string;
+ description: string;
+};
+
+// endpointNameSchema is the endpoint name rule plus the check the manager makes
+// on submit — no two endpoints of one Runtime share a name — so a clash is
+// caught as it is typed rather than after the review.
+export function endpointNameSchema(runtime: ProjectRuntime | undefined): z.ZodType {
+ const taken = new Set(Object.keys(runtime?.endpoints ?? {}));
+ return RuntimeEndpointNameSchema.superRefine((name, ctx) => {
+ if (runtime !== undefined && taken.has(name)) {
+ ctx.addIssue({
+ code: "custom",
+ message: `an endpoint named '${name}' already exists on runtime '${runtime.name}'`,
+ });
+ }
+ });
+}
+
+// toRuntimeEndpointInput is the answers as the flag path would state them: the
+// version parsed from its digits, a blank description left out.
+export function toRuntimeEndpointInput(values: RuntimeEndpointFormValues): RuntimeEndpointInput {
+ const description = values.description.trim();
+ return {
+ runtime: values.runtime,
+ name: values.name,
+ version: Number(values.version),
+ description: description === "" ? undefined : description,
+ };
+}
+
+function summaryOf(values: RuntimeEndpointFormValues): Record {
+ return {
+ runtime: values.runtime,
+ endpoint: values.name,
+ version: values.version,
+ description: values.description.trim() || "(none)",
+ };
+}
+
+// runtimeChoices lists each Runtime with the endpoints it already has, so the
+// name step's uniqueness rule is no surprise.
+function runtimeChoices(runtimes: readonly ProjectRuntime[]): Choice[] {
+ return runtimes.map((runtime) => {
+ const names = Object.keys(runtime.endpoints ?? {});
+ return {
+ value: runtime.name,
+ label: runtime.name,
+ description:
+ names.length === 0
+ ? "no endpoints yet"
+ : `${names.length === 1 ? "endpoint" : "endpoints"}: ${names.join(", ")}`,
+ };
+ });
+}
+
+export function AddRuntimeEndpointScreen({ ctx, core }: ScreenProps) {
+ const navigate = useNavigate();
+ return (
+ navigate(ADD_MENU)}
+ >
+ {(project) => }
+
+ );
+}
+
+function AddRuntimeEndpointWizard({
+ project,
+ core,
+}: {
+ project: Project;
+ core: ScreenProps["core"];
+}) {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const runtimes = project.spec.runtimes;
+ const [values, setValues] = useState({
+ runtime: runtimes[0]?.name ?? "",
+ name: "",
+ version: String(DEFAULT_ENDPOINT_VERSION),
+ description: "",
+ });
+ const set = (update: Partial) =>
+ setValues((current) => ({ ...current, ...update }));
+ const runtime = runtimes.find((candidate) => candidate.name === values.runtime);
+
+ return (
+ navigate(ADD_MENU)}
+ onSubmit={async function* () {
+ const updated = yield* core.projectManager.addResource(
+ project,
+ toAddRuntimeEndpointInput(toRuntimeEndpointInput(values)),
+ );
+ queryClient.setQueryData(projectQueryKey(), updated);
+ return updated;
+ }}
+ runningLabel={`adding runtime endpoint ${values.name}…`}
+ successLabel={`added runtime endpoint '${values.name}' (version ${values.version}) to runtime '${values.runtime}' in '${project.name}'`}
+ successNextSteps={["agentcore deploy"]}
+ onDone={() => navigate(ADD_MENU)}
+ doneLabel="go back"
+ >
+
+ set({ runtime })}
+ emptyMessage="no Runtimes in this project"
+ emptyHint="add one with agentcore add runtime"
+ />
+
+
+
+ set({ name })}
+ required
+ schema={endpointNameSchema(runtime)}
+ live
+ />
+
+
+
+ set({ version }),
+ required: true,
+ number: true,
+ schema: VERSION_SCHEMA,
+ },
+ {
+ key: "description",
+ label: "Description",
+ help: "optional · up to 200 characters",
+ placeholder: "production traffic",
+ value: values.description,
+ onChange: (description) => set({ description }),
+ schema: RuntimeEndpointSchema.shape.description,
+ },
+ ]}
+ />
+
+
+
+
+
+
+ );
+}