diff --git a/README.md b/README.md index 79822cc32..b48d2b575 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ headless). A bare `project status` opens a Linked Resources view that groups the project's resources by agent and forwards to each deployed resource's detail page. The harness hub (`harness get`) ends with the same kind of Linked Resources tree for the Runtime, Memory, Gateway, Browser, Code Interpreter and -credential providers wired to that harness, each opening in its own region. +credential providers wired to that harness, each opening in its own region; the +region then follows that resource through its actions, lists and detail views. ``` agentcore # interactive TUI diff --git a/src/components/AbTestPicker.tsx b/src/components/AbTestPicker.tsx index c27a53209..56722a8dd 100644 --- a/src/components/AbTestPicker.tsx +++ b/src/components/AbTestPicker.tsx @@ -1,7 +1,7 @@ import type { ABTestSummary } from "@aws-sdk/client-bedrock-agentcore"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -47,7 +47,7 @@ export function AbTestPicker({ onSelect, onEscape, }: AbTestPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/BatchEvaluationPicker.tsx b/src/components/BatchEvaluationPicker.tsx index 0b202af98..e0619a61b 100644 --- a/src/components/BatchEvaluationPicker.tsx +++ b/src/components/BatchEvaluationPicker.tsx @@ -1,7 +1,7 @@ import type { BatchEvaluationSummary } from "@aws-sdk/client-bedrock-agentcore"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -58,7 +58,7 @@ export function BatchEvaluationPicker({ onSelect, onEscape, }: BatchEvaluationPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/BatchInsightsPicker.tsx b/src/components/BatchInsightsPicker.tsx index 649a146a0..eb587ce1b 100644 --- a/src/components/BatchInsightsPicker.tsx +++ b/src/components/BatchInsightsPicker.tsx @@ -1,7 +1,7 @@ import type { BatchEvaluationSummary } from "@aws-sdk/client-bedrock-agentcore"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -49,7 +49,7 @@ export function BatchInsightsPicker({ onSelect, onEscape, }: BatchInsightsPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/ConfigBundlePicker.tsx b/src/components/ConfigBundlePicker.tsx index 19572d11b..f70c48ea2 100644 --- a/src/components/ConfigBundlePicker.tsx +++ b/src/components/ConfigBundlePicker.tsx @@ -1,7 +1,7 @@ import type { ConfigurationBundleSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -50,7 +50,7 @@ export function ConfigBundlePicker({ onSelect, onEscape, }: ConfigBundlePickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/ConfigBundleVersionPicker.tsx b/src/components/ConfigBundleVersionPicker.tsx index b1a7102cb..02e76c228 100644 --- a/src/components/ConfigBundleVersionPicker.tsx +++ b/src/components/ConfigBundleVersionPicker.tsx @@ -1,6 +1,6 @@ import type { ConfigurationBundleVersionSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -49,7 +49,7 @@ export function ConfigBundleVersionPicker({ onSelect, onBack, }: ConfigBundleVersionPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return ( navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/EndpointWizard.tsx b/src/components/EndpointWizard.tsx index 74372e8f2..519883af5 100644 --- a/src/components/EndpointWizard.tsx +++ b/src/components/EndpointWizard.tsx @@ -1,13 +1,12 @@ import { useMemo, useState } from "react"; import { Box, Text, useInput } from "ink"; -import { useNavigate } from "react-router"; import { useQuery } from "@tanstack/react-query"; import type { CreateHarnessEndpointRequest, UpdateHarnessEndpointRequest, } from "@aws-sdk/client-bedrock-agentcore-control"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts, useRegionNavigate } from "../handlers/utils"; import { Layout } from "./Layout"; import { FormRadioGroup, type FormRadioOption } from "./FormRadioGroup"; import { FormTextInput } from "./FormTextInput"; @@ -60,8 +59,8 @@ export function EndpointWizard({ initial, onDone, }: EndpointWizardProps) { - const navigate = useNavigate(); - const opts = coreOptsFromCtx(ctx); + const navigate = useRegionNavigate(); + const opts = useCoreOpts(ctx); const steps: Step[] = useMemo(() => { const all: Step[] = [ diff --git a/src/components/EvaluatorPicker.tsx b/src/components/EvaluatorPicker.tsx index d0b1ebec7..33a6d9d18 100644 --- a/src/components/EvaluatorPicker.tsx +++ b/src/components/EvaluatorPicker.tsx @@ -1,7 +1,7 @@ import type { EvaluatorSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -62,7 +62,7 @@ export function EvaluatorPicker({ onSelect, onEscape, }: EvaluatorPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/GatewayConnectorPicker.tsx b/src/components/GatewayConnectorPicker.tsx index 17f115ce7..794373c9c 100644 --- a/src/components/GatewayConnectorPicker.tsx +++ b/src/components/GatewayConnectorPicker.tsx @@ -1,7 +1,7 @@ import type { TargetSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -48,7 +48,7 @@ export function GatewayConnectorPicker({ description, onSelect, }: GatewayConnectorPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); return ( diff --git a/src/components/GatewayPicker.tsx b/src/components/GatewayPicker.tsx index ec688260c..9ff92cfae 100644 --- a/src/components/GatewayPicker.tsx +++ b/src/components/GatewayPicker.tsx @@ -1,7 +1,7 @@ import type { GatewaySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -54,7 +54,7 @@ export function GatewayPicker({ onSelect, onEscape, }: GatewayPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); return ( diff --git a/src/components/GatewayRulePicker.tsx b/src/components/GatewayRulePicker.tsx index 17cfb6ddb..12924ca17 100644 --- a/src/components/GatewayRulePicker.tsx +++ b/src/components/GatewayRulePicker.tsx @@ -1,7 +1,7 @@ import type { GatewayRuleDetail } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -48,7 +48,7 @@ export function GatewayRulePicker({ description, onSelect, }: GatewayRulePickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); return ( diff --git a/src/components/GatewayTargetPicker.tsx b/src/components/GatewayTargetPicker.tsx index 19e5eb2ec..20780137e 100644 --- a/src/components/GatewayTargetPicker.tsx +++ b/src/components/GatewayTargetPicker.tsx @@ -1,7 +1,7 @@ import type { TargetSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -51,7 +51,7 @@ export function GatewayTargetPicker({ description, onSelect, }: GatewayTargetPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); return ( diff --git a/src/components/HarnessEndpointPicker.tsx b/src/components/HarnessEndpointPicker.tsx index fae3f139a..d392e0151 100644 --- a/src/components/HarnessEndpointPicker.tsx +++ b/src/components/HarnessEndpointPicker.tsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router"; import type { HarnessEndpoint } from "@aws-sdk/client-bedrock-agentcore-control"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -68,7 +68,7 @@ export function HarnessEndpointPicker({ onSelect, onEscape, }: HarnessEndpointPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate(-1)); diff --git a/src/components/HarnessPicker.tsx b/src/components/HarnessPicker.tsx index 89cd8a4d5..22e49da0d 100644 --- a/src/components/HarnessPicker.tsx +++ b/src/components/HarnessPicker.tsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router"; import type { HarnessSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -65,7 +65,7 @@ export function HarnessPicker({ description, onSelect, }: HarnessPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = () => navigate("/" + breadcrumb.slice(0, -1).join("/")); diff --git a/src/components/HarnessVersionPicker.tsx b/src/components/HarnessVersionPicker.tsx index 8a35ac126..1d8b90d80 100644 --- a/src/components/HarnessVersionPicker.tsx +++ b/src/components/HarnessVersionPicker.tsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router"; import type { HarnessVersionSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -59,7 +59,7 @@ export function HarnessVersionPicker({ description, onSelect, }: HarnessVersionPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = () => navigate(-1); diff --git a/src/components/HarnessWizard.tsx b/src/components/HarnessWizard.tsx index 5be985378..44adb9f6b 100644 --- a/src/components/HarnessWizard.tsx +++ b/src/components/HarnessWizard.tsx @@ -8,7 +8,7 @@ import type { } from "@aws-sdk/client-bedrock-agentcore-control"; import type { CreateHarnessInput } from "../handlers/harness/types"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { Layout } from "./Layout"; import { ErrorPanel } from "./ErrorPanel"; import { FormTextInput } from "./FormTextInput"; @@ -268,7 +268,7 @@ export function HarnessWizard({ onDone, onExit, }: HarnessWizardProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const steps: Step[] = useMemo(() => { const all: Step[] = [ diff --git a/src/components/MemoryPicker.tsx b/src/components/MemoryPicker.tsx index 98005d6b9..fc234a6f6 100644 --- a/src/components/MemoryPicker.tsx +++ b/src/components/MemoryPicker.tsx @@ -1,7 +1,7 @@ import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -38,7 +38,7 @@ export interface MemoryPickerProps extends ScreenProps { } export function MemoryPicker({ ctx, core, breadcrumb, description, onSelect }: MemoryPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = () => navigate("/" + breadcrumb.slice(0, -1).join("/")); diff --git a/src/components/OnlineEvalPicker.tsx b/src/components/OnlineEvalPicker.tsx index 008225bcd..5fe4da1b6 100644 --- a/src/components/OnlineEvalPicker.tsx +++ b/src/components/OnlineEvalPicker.tsx @@ -1,7 +1,7 @@ import type { OnlineEvaluationConfigSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -63,7 +63,7 @@ export function OnlineEvalPicker({ onSelect, onEscape, }: OnlineEvalPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/OnlineInsightPicker.tsx b/src/components/OnlineInsightPicker.tsx index 2b5f57b99..3c851bd5b 100644 --- a/src/components/OnlineInsightPicker.tsx +++ b/src/components/OnlineInsightPicker.tsx @@ -1,7 +1,7 @@ import type { OnlineEvaluationConfigSummary } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -63,7 +63,7 @@ export function OnlineInsightPicker({ onSelect, onEscape, }: OnlineInsightPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/RuntimeEndpointPicker.tsx b/src/components/RuntimeEndpointPicker.tsx index fcc053124..37cf980ca 100644 --- a/src/components/RuntimeEndpointPicker.tsx +++ b/src/components/RuntimeEndpointPicker.tsx @@ -1,7 +1,7 @@ import type { AgentRuntimeEndpoint } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -54,7 +54,7 @@ export function RuntimeEndpointPicker({ onSelect, onEscape, }: RuntimeEndpointPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate(-1)); diff --git a/src/components/RuntimePicker.tsx b/src/components/RuntimePicker.tsx index a057ab4c6..71f0b04b9 100644 --- a/src/components/RuntimePicker.tsx +++ b/src/components/RuntimePicker.tsx @@ -1,7 +1,7 @@ import type { AgentRuntime } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -52,7 +52,7 @@ export function RuntimePicker({ onSelect, onEscape, }: RuntimePickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/"))); diff --git a/src/components/RuntimeVersionPicker.tsx b/src/components/RuntimeVersionPicker.tsx index 64954348a..12b3f35ba 100644 --- a/src/components/RuntimeVersionPicker.tsx +++ b/src/components/RuntimeVersionPicker.tsx @@ -1,7 +1,7 @@ import type { AgentRuntime } from "@aws-sdk/client-bedrock-agentcore-control"; import { useNavigate } from "react-router"; import type { ScreenProps } from "../handlers/types"; -import { coreOptsFromCtx } from "../handlers/utils"; +import { useCoreOpts } from "../handlers/utils"; import { formatTimestamp } from "./formatTimestamp"; import { PaginatedTablePicker } from "./PaginatedTablePicker"; import type { DataTableColumn } from "./ui/data-table"; @@ -47,7 +47,7 @@ export function RuntimeVersionPicker({ description, onSelect, }: RuntimeVersionPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const navigate = useNavigate(); const goBack = () => navigate(-1); diff --git a/src/handlers/eval/ab-test/get/screen.tsx b/src/handlers/eval/ab-test/get/screen.tsx index 782013d3b..37deda036 100644 --- a/src/handlers/eval/ab-test/get/screen.tsx +++ b/src/handlers/eval/ab-test/get/screen.tsx @@ -3,10 +3,10 @@ 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"; +import { useCoreOpts } from "../../../utils"; function useAbTestDetail({ ctx, core }: ScreenProps, abTestId: string | undefined) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["ab-test", opts.region, abTestId], queryFn: () => core.eval.getABTest(abTestId!, opts), diff --git a/src/handlers/eval/batch-evaluation/get/screen.tsx b/src/handlers/eval/batch-evaluation/get/screen.tsx index c8fe68e8e..7d94cdbd7 100644 --- a/src/handlers/eval/batch-evaluation/get/screen.tsx +++ b/src/handlers/eval/batch-evaluation/get/screen.tsx @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; // getBatchEvaluation already fetches the job and merges the per-session CloudWatch // results, returning `{ detail, resultsError? }`. The screen keeps both: a @@ -10,7 +10,7 @@ import { coreOptsFromCtx } from "../../../utils"; // that simply has none yet, so `resultsError` drives a warning banner (the TUI's // equivalent of the CLI's stderr warning). function useBatchEvaluationDetail({ ctx, core }: ScreenProps, id: string | undefined) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["batch-evaluation", opts.region, id], queryFn: () => core.eval.getBatchEvaluation(id!, opts), diff --git a/src/handlers/eval/batch-insights/get/screen.tsx b/src/handlers/eval/batch-insights/get/screen.tsx index 03340dbdf..f26d2e535 100644 --- a/src/handlers/eval/batch-insights/get/screen.tsx +++ b/src/handlers/eval/batch-insights/get/screen.tsx @@ -2,10 +2,10 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; function useBatchInsightsDetail({ ctx, core }: ScreenProps, id: string | undefined) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["batch-insights", opts.region, id], queryFn: () => core.eval.getBatchInsights(id!, opts), diff --git a/src/handlers/eval/config-bundle/get/screen.tsx b/src/handlers/eval/config-bundle/get/screen.tsx index 007ebe09f..8605d8220 100644 --- a/src/handlers/eval/config-bundle/get/screen.tsx +++ b/src/handlers/eval/config-bundle/get/screen.tsx @@ -2,12 +2,12 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; const DEFAULT_BRANCH = "mainline"; export function ConfigBundleGetScreen({ ctx, core }: ScreenProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const { bundleId, versionId } = useParams(); const detail = useQuery({ queryKey: ["configuration-bundle", opts.region, bundleId, versionId ?? DEFAULT_BRANCH], diff --git a/src/handlers/eval/dataset/get/screen.tsx b/src/handlers/eval/dataset/get/screen.tsx index b88386ab0..e536739ac 100644 --- a/src/handlers/eval/dataset/get/screen.tsx +++ b/src/handlers/eval/dataset/get/screen.tsx @@ -3,10 +3,10 @@ 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"; +import { useCoreOpts } from "../../../utils"; function useDatasetDetail({ ctx, core }: ScreenProps, datasetId: string | undefined) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["dataset", opts.region, datasetId], queryFn: () => core.eval.getDataset(datasetId!, undefined, opts), diff --git a/src/handlers/eval/evaluator/get/screen.tsx b/src/handlers/eval/evaluator/get/screen.tsx index e94830d1e..690ac9cc1 100644 --- a/src/handlers/eval/evaluator/get/screen.tsx +++ b/src/handlers/eval/evaluator/get/screen.tsx @@ -4,10 +4,10 @@ import type { GetEvaluatorResponse } from "@aws-sdk/client-bedrock-agentcore-con import { JsonDetail } from "../../../../components/JsonDetail"; import { ResourceDetailScreen } from "../../../../components/ResourceDetailScreen"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; function useEvaluatorDetail({ ctx, core }: ScreenProps, evaluatorId: string | undefined) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["evaluator", opts.region, evaluatorId], queryFn: () => core.eval.getEvaluator(evaluatorId!, opts), diff --git a/src/handlers/eval/online-eval/get/screen.tsx b/src/handlers/eval/online-eval/get/screen.tsx index bd7e7a220..26e2cedb6 100644 --- a/src/handlers/eval/online-eval/get/screen.tsx +++ b/src/handlers/eval/online-eval/get/screen.tsx @@ -3,10 +3,10 @@ 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"; +import { useCoreOpts } from "../../../utils"; function useOnlineEvalDetail({ ctx, core }: ScreenProps, configId: string | undefined) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["online-eval", opts.region, configId], queryFn: () => core.eval.getOnlineEvaluationConfig(configId!, opts), diff --git a/src/handlers/eval/online-insight/get/screen.tsx b/src/handlers/eval/online-insight/get/screen.tsx index 11d156570..3ce4b40e1 100644 --- a/src/handlers/eval/online-insight/get/screen.tsx +++ b/src/handlers/eval/online-insight/get/screen.tsx @@ -3,10 +3,10 @@ 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"; +import { useCoreOpts } from "../../../utils"; function useOnlineInsightDetail({ ctx, core }: ScreenProps, configId: string | undefined) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["online-insight", opts.region, configId], queryFn: () => core.eval.getOnlineInsight(configId!, opts), diff --git a/src/handlers/gateway/connector/get/screen.tsx b/src/handlers/gateway/connector/get/screen.tsx index f55eb2435..ffbcaa28e 100644 --- a/src/handlers/gateway/connector/get/screen.tsx +++ b/src/handlers/gateway/connector/get/screen.tsx @@ -2,11 +2,11 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; export function GatewayConnectorGetScreen(props: ScreenProps) { const { gatewayId, targetId } = useParams(); - const opts = coreOptsFromCtx(props.ctx); + const opts = useCoreOpts(props.ctx); const detail = useQuery({ queryKey: ["gateway-connector", opts.region, gatewayId, targetId], queryFn: () => props.core.gateway.getGatewayConnector(gatewayId!, targetId!, opts), diff --git a/src/handlers/gateway/connector/list/screen.tsx b/src/handlers/gateway/connector/list/screen.tsx index 5273cc937..24fb4cc2f 100644 --- a/src/handlers/gateway/connector/list/screen.tsx +++ b/src/handlers/gateway/connector/list/screen.tsx @@ -1,11 +1,12 @@ -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { GatewayConnectorPicker } from "../../../../components/GatewayConnectorPicker"; import { GatewayPicker } from "../../../../components/GatewayPicker"; import type { ScreenProps } from "../../../types"; +import { useRegionNavigate } from "../../../utils"; export function GatewayConnectorListScreen(props: ScreenProps) { const { gatewayId } = useParams(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); if (!gatewayId) { return ( diff --git a/src/handlers/gateway/gateway.screen.test.tsx b/src/handlers/gateway/gateway.screen.test.tsx index 574f4f60a..a5610ad8b 100644 --- a/src/handlers/gateway/gateway.screen.test.tsx +++ b/src/handlers/gateway/gateway.screen.test.tsx @@ -11,10 +11,11 @@ import { } from "@aws-sdk/client-bedrock-agentcore-control"; import { cleanupScreens, + menuEntries, renderScreen, TestCoreClient, + waitFor, waitForText, - menuEntries, } from "../../testing"; afterEach(cleanupScreens); @@ -315,6 +316,31 @@ describe("Gateway Target flow", () => { await screen.press("escape"); await waitForText(screen.lastFrame, "manage Targets for an AgentCore Gateway"); }); + + // A hub reached with ?region= (from project status or a harness's linked + // rows) fetches there; its actions must keep fetching there too. + test("a Gateway hub opened with ?region= carries the region into its Target list", async () => { + const core = new TestCoreClient(); + core.gateway.setGetResponse(gatewayDetail()); + core.gateway.setListTargetsResponse({ items: [] }); + const screen = renderScreen( + `/agentcore/gateway/get/${encodeURIComponent(GATEWAY_ID)}?region=eu-west-1`, + { core }, + ); + + await waitForText(screen.lastFrame, "browse every Target"); + expect(core.gateway.calls.find((call) => call.method === "getGateway")?.args[1]).toMatchObject({ + region: "eu-west-1", + }); + + await screen.press("down"); + await screen.press("return"); + await waitForText(screen.lastFrame, `agentcore → gateway → target → list → ${GATEWAY_ID}`); + await waitFor(() => core.gateway.calls.some((call) => call.method === "listGatewayTargets")); + expect( + core.gateway.calls.find((call) => call.method === "listGatewayTargets")?.args[3], + ).toMatchObject({ region: "eu-west-1" }); + }); }); describe("Gateway Connector flow", () => { diff --git a/src/handlers/gateway/get/screen.tsx b/src/handlers/gateway/get/screen.tsx index 4e4e0b0b1..8f0271a94 100644 --- a/src/handlers/gateway/get/screen.tsx +++ b/src/handlers/gateway/get/screen.tsx @@ -1,9 +1,9 @@ import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { JsonDetail } from "../../../components/JsonDetail"; import { ResourceDetailScreen } from "../../../components/ResourceDetailScreen"; import type { ScreenProps } from "../../types"; -import { useCoreOpts } from "../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../utils"; function useGatewayDetail({ ctx, core }: ScreenProps, gatewayId: string | undefined) { const opts = useCoreOpts(ctx); @@ -15,7 +15,7 @@ function useGatewayDetail({ ctx, core }: ScreenProps, gatewayId: string | undefi } export function GatewayGetScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { gatewayId } = useParams(); const detail = useGatewayDetail(props, gatewayId); diff --git a/src/handlers/gateway/invoke/screen.tsx b/src/handlers/gateway/invoke/screen.tsx index 6cdd905e9..6875c47e6 100644 --- a/src/handlers/gateway/invoke/screen.tsx +++ b/src/handlers/gateway/invoke/screen.tsx @@ -5,7 +5,7 @@ import cliTruncate from "cli-truncate"; import { Box, Text, useInput, useWindowSize } from "ink"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; import { useCallback, useEffect, useRef, useState } from "react"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { GatewayPicker } from "../../../components/GatewayPicker"; import { Layout } from "../../../components/Layout"; import { MultilineInput } from "../../../components/MultilineInput"; @@ -15,7 +15,7 @@ import { Spinner } from "../../../components/ui/spinner"; import { TextInput } from "../../../components/ui/text-input"; import { classifyStreamingResponse } from "../../../io"; import type { ScreenProps } from "../../types"; -import { coreOptsFromCtx } from "../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../utils"; import type { GatewayInvokeRequest, GatewayInvokeResponse } from "../types"; import { GatewayInvokeLaunchContextKey, type GatewayInvokeLaunchContext } from "./launchContext"; import { normalizeGatewayInvokeRequest } from "./request"; @@ -140,7 +140,7 @@ function PathEditor({ export function GatewayInvokeScreen(props: ScreenProps) { const { gatewayId } = useParams(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const launchContext = props.ctx.value(GatewayInvokeLaunchContextKey); const initialContext = launchContext?.gatewayId === gatewayId ? launchContext : undefined; @@ -164,8 +164,8 @@ type GatewayInvokeConsoleProps = ScreenProps & { }; function GatewayInvokeConsole({ ctx, core, gatewayId, initialContext }: GatewayInvokeConsoleProps) { - const navigate = useNavigate(); - const opts = coreOptsFromCtx(ctx); + const navigate = useRegionNavigate(); + const opts = useCoreOpts(ctx); const { columns, rows } = useWindowSize(); const [targetGatewayId, setTargetGatewayId] = useState(gatewayId); const [pickingGateway, setPickingGateway] = useState(false); diff --git a/src/handlers/gateway/list/screen.tsx b/src/handlers/gateway/list/screen.tsx index 05828d2fb..0fa8e9135 100644 --- a/src/handlers/gateway/list/screen.tsx +++ b/src/handlers/gateway/list/screen.tsx @@ -1,9 +1,9 @@ -import { useNavigate } from "react-router"; import { GatewayPicker } from "../../../components/GatewayPicker"; import type { ScreenProps } from "../../types"; +import { useRegionNavigate } from "../../utils"; export function GatewayListScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); return ( core.gateway.getGateway(gatewayId, opts), diff --git a/src/handlers/gateway/rule/get/screen.tsx b/src/handlers/gateway/rule/get/screen.tsx index b85dc7dab..ce29c92fb 100644 --- a/src/handlers/gateway/rule/get/screen.tsx +++ b/src/handlers/gateway/rule/get/screen.tsx @@ -2,11 +2,11 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; export function GatewayRuleGetScreen(props: ScreenProps) { const { gatewayId, ruleId } = useParams(); - const opts = coreOptsFromCtx(props.ctx); + const opts = useCoreOpts(props.ctx); const detail = useQuery({ queryKey: ["gateway-rule", opts.region, gatewayId, ruleId], queryFn: () => props.core.gateway.getGatewayRule(gatewayId!, ruleId!, opts), diff --git a/src/handlers/gateway/rule/list/screen.tsx b/src/handlers/gateway/rule/list/screen.tsx index 787317271..d8bf47e5b 100644 --- a/src/handlers/gateway/rule/list/screen.tsx +++ b/src/handlers/gateway/rule/list/screen.tsx @@ -1,11 +1,12 @@ -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { GatewayPicker } from "../../../../components/GatewayPicker"; import { GatewayRulePicker } from "../../../../components/GatewayRulePicker"; import type { ScreenProps } from "../../../types"; +import { useRegionNavigate } from "../../../utils"; export function GatewayRuleListScreen(props: ScreenProps) { const { gatewayId } = useParams(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); if (!gatewayId) { return ( diff --git a/src/handlers/gateway/target/list/screen.tsx b/src/handlers/gateway/target/list/screen.tsx index 94f833447..8c8ce3522 100644 --- a/src/handlers/gateway/target/list/screen.tsx +++ b/src/handlers/gateway/target/list/screen.tsx @@ -1,11 +1,12 @@ -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { GatewayPicker } from "../../../../components/GatewayPicker"; import { GatewayTargetPicker } from "../../../../components/GatewayTargetPicker"; import type { ScreenProps } from "../../../types"; +import { useRegionNavigate } from "../../../utils"; export function GatewayTargetListScreen(props: ScreenProps) { const { gatewayId } = useParams(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); if (!gatewayId) { return ( diff --git a/src/handlers/harness/delete/screen.tsx b/src/handlers/harness/delete/screen.tsx index f939e3ab6..6b05a0d34 100644 --- a/src/handlers/harness/delete/screen.tsx +++ b/src/handlers/harness/delete/screen.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { ScreenProps } from "../../types"; -import { coreOptsFromCtx } from "../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../utils"; import { HarnessPicker } from "../../../components/HarnessPicker"; import { ConfirmAction } from "../../../components/ConfirmAction"; @@ -9,7 +9,7 @@ import { ConfirmAction } from "../../../components/ConfirmAction"; // renders a harness picker; with one it shows the harness summary and asks for // confirmation before calling DeleteHarness. export function HarnessDeleteScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { harnessId } = useParams(); if (!harnessId) { @@ -26,8 +26,8 @@ export function HarnessDeleteScreen(props: ScreenProps) { } function DeleteConfirm({ ctx, core, harnessId }: ScreenProps & { harnessId: string }) { - const opts = coreOptsFromCtx(ctx); - const navigate = useNavigate(); + const opts = useCoreOpts(ctx); + const navigate = useRegionNavigate(); const detail = useQuery({ queryKey: ["harness", opts.region, harnessId], diff --git a/src/handlers/harness/endpoint/create/screen.tsx b/src/handlers/harness/endpoint/create/screen.tsx index 765653ccb..f31976d4f 100644 --- a/src/handlers/harness/endpoint/create/screen.tsx +++ b/src/handlers/harness/endpoint/create/screen.tsx @@ -1,15 +1,16 @@ -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { ScreenProps } from "../../../types"; import { HarnessPicker } from "../../../../components/HarnessPicker"; import { EndpointWizard } from "../../../../components/EndpointWizard"; import { useFinishFlow } from "../../../../components/useFinishFlow"; +import { useRegionNavigate } from "../../../utils"; // HarnessCreateEndpointScreen is the interactive endpoint create flow. Without // a `:harnessId` route value it renders a harness picker; with one it runs the // endpoint wizard (name → version → review) ending in a // CreateHarnessEndpoint call. Success lands on the endpoint's detail. export function HarnessCreateEndpointScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const finishFlow = useFinishFlow("/agentcore/harness/endpoint"); const { harnessId } = useParams(); diff --git a/src/handlers/harness/endpoint/delete/screen.tsx b/src/handlers/harness/endpoint/delete/screen.tsx index d76618650..51a129a12 100644 --- a/src/handlers/harness/endpoint/delete/screen.tsx +++ b/src/handlers/harness/endpoint/delete/screen.tsx @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../../utils"; import { HarnessPicker } from "../../../../components/HarnessPicker"; import { HarnessEndpointPicker } from "../../../../components/HarnessEndpointPicker"; import { ConfirmAction } from "../../../../components/ConfirmAction"; @@ -11,7 +11,7 @@ import { useFinishFlow } from "../../../../components/useFinishFlow"; // from a harness picker to an endpoint picker to a confirmation, then calls // DeleteHarnessEndpoint. export function HarnessDeleteEndpointScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { harnessId, endpointName } = useParams(); if (!harnessId) { @@ -44,7 +44,7 @@ function DeleteConfirm({ harnessId, endpointName, }: ScreenProps & { harnessId: string; endpointName: string }) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const finishFlow = useFinishFlow("/agentcore/harness/endpoint"); const detail = useQuery({ diff --git a/src/handlers/harness/endpoint/get/screen.tsx b/src/handlers/harness/endpoint/get/screen.tsx index 4884e5520..bf74db8be 100644 --- a/src/handlers/harness/endpoint/get/screen.tsx +++ b/src/handlers/harness/endpoint/get/screen.tsx @@ -1,13 +1,13 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; import { JsonDetail } from "../../../../components/JsonDetail"; // HarnessGetEndpointScreen shows one endpoint's full definition as scrollable // JSON. The harness ID and endpoint name come from the route path values. export function HarnessGetEndpointScreen({ ctx, core }: ScreenProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const { harnessId, endpointName } = useParams(); const detail = useQuery({ diff --git a/src/handlers/harness/endpoint/list/screen.tsx b/src/handlers/harness/endpoint/list/screen.tsx index 29fbd76e9..83df07c76 100644 --- a/src/handlers/harness/endpoint/list/screen.tsx +++ b/src/handlers/harness/endpoint/list/screen.tsx @@ -1,13 +1,14 @@ -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { ScreenProps } from "../../../types"; import { HarnessPicker } from "../../../../components/HarnessPicker"; import { HarnessEndpointPicker } from "../../../../components/HarnessEndpointPicker"; +import { useRegionNavigate } from "../../../utils"; // HarnessListEndpointsScreen lists a harness's endpoints. Without a `:harnessId` // route value it renders a harness picker first; with one it lists that // harness's endpoints, and selecting an endpoint opens its JSON detail. export function HarnessListEndpointsScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { harnessId } = useParams(); if (!harnessId) { diff --git a/src/handlers/harness/endpoint/update/screen.tsx b/src/handlers/harness/endpoint/update/screen.tsx index 580ebaaa0..c4c9bd99c 100644 --- a/src/handlers/harness/endpoint/update/screen.tsx +++ b/src/handlers/harness/endpoint/update/screen.tsx @@ -1,8 +1,8 @@ import { Text } from "ink"; import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../../utils"; import { HarnessPicker } from "../../../../components/HarnessPicker"; import { HarnessEndpointPicker } from "../../../../components/HarnessEndpointPicker"; import { EndpointWizard } from "../../../../components/EndpointWizard"; @@ -14,7 +14,7 @@ import { useFinishFlow } from "../../../../components/useFinishFlow"; // harness, pick the endpoint, then run the endpoint wizard prefilled with the // endpoint's current target version, ending in an UpdateHarnessEndpoint call. export function HarnessUpdateEndpointScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { harnessId, endpointName } = useParams(); if (!harnessId) { @@ -47,7 +47,7 @@ function UpdateWizard({ harnessId, endpointName, }: ScreenProps & { harnessId: string; endpointName: string }) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const finishFlow = useFinishFlow("/agentcore/harness/endpoint"); // The wizard seeds its form state once on mount, so it renders only after the diff --git a/src/handlers/harness/exec/screen.tsx b/src/handlers/harness/exec/screen.tsx index 6130b2ee8..bea300a63 100644 --- a/src/handlers/harness/exec/screen.tsx +++ b/src/handlers/harness/exec/screen.tsx @@ -1,7 +1,8 @@ -import { useNavigate, useParams, useSearchParams } from "react-router"; +import { useParams, useSearchParams } from "react-router"; import type { ScreenProps } from "../../types"; import { HarnessPicker } from "../../../components/HarnessPicker"; import { HarnessChat } from "../invoke/screen"; +import { useRegionNavigate } from "../../utils"; // HarnessExecScreen is `harness exec` in the TUI: the same chat screen as // invoke, but starting in exec mode ($ prompt, enter runs a shell command in @@ -11,7 +12,7 @@ import { HarnessChat } from "../invoke/screen"; export function HarnessExecScreen(props: ScreenProps) { const { harnessId, sessionId } = useParams(); const [search] = useSearchParams(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); if (!harnessId) { return ( diff --git a/src/handlers/harness/get/get.screen.test.tsx b/src/handlers/harness/get/get.screen.test.tsx index be98aab74..de169d4a8 100644 --- a/src/handlers/harness/get/get.screen.test.tsx +++ b/src/handlers/harness/get/get.screen.test.tsx @@ -176,6 +176,30 @@ describe("harness hub screen", () => { await waitFor(() => core.harness.calls.some((c) => c.method === "listHarnesses")); r.unmount(); }); + + // A hub reached with ?region= (from project status) fetches there; its + // actions must keep fetching there too. + test("a hub opened with ?region= carries the region into its endpoint list", async () => { + const core = new TestCoreClient(); + core.harness.setGetResponse(getResponse()); + core.harness.setListEndpointsResponse({ endpoints: [] }); + const r = renderScreen("/agentcore/harness/get/MyHarness-abc123?region=us-west-2", { core }); + + await waitForText(r.lastFrame, "detail"); + const get = core.harness.calls.find((c) => c.method === "getHarness")!; + expect(get.args[1]).toMatchObject({ region: "us-west-2" }); + + await r.press("down"); + await r.press("return"); + await waitForText(r.lastFrame, "agentcore → harness → endpoint → list → MyHarness-abc123"); + await waitFor(() => core.harness.calls.some((c) => c.method === "listHarnessEndpoints")); + const list = core.harness.calls.find((c) => c.method === "listHarnessEndpoints")!; + const options = list.args.find( + (arg) => typeof arg === "object" && arg !== null && "region" in arg, + ); + expect(options).toMatchObject({ region: "us-west-2" }); + r.unmount(); + }); }); // The linked resources live in us-west-2 while the test context's ambient @@ -482,6 +506,26 @@ describe("harness hub linked resources", () => { expect(r.lastFrame()).toContain("collapse/expand"); r.unmount(); }); + + test("a linked runtime's region follows it into the runtime's detail JSON", async () => { + const { core, r } = linkedHubScreen(); + + await waitForText(r.lastFrame, "linked resources"); + await focusTree(r); + await r.press("return"); + await waitForText(r.lastFrame, `agentcore → runtime → get → ${RUNTIME_ID}`); + await waitForText(r.lastFrame, "show the full JSON definition"); + // invoke → shell → endpoints → versions → detail + for (let press = 0; press < 4; press++) await r.press("down"); + await r.press("return"); + + await waitForText(r.lastFrame, `agentcore → runtime → get → ${RUNTIME_ID} → json`); + await waitForText(r.lastFrame, '"agentRuntimeId"'); + const fetches = core.runtime.calls.filter(({ method }) => method === "getRuntime"); + expect(fetches.length).toBeGreaterThanOrEqual(2); + for (const fetch of fetches) expect(fetch.args[1]).toMatchObject({ region: LINK_REGION }); + r.unmount(); + }); }); describe("buildHarnessLinkNodes", () => { diff --git a/src/handlers/harness/get/screen.tsx b/src/handlers/harness/get/screen.tsx index e8a9f1cdf..512b67db9 100644 --- a/src/handlers/harness/get/screen.tsx +++ b/src/handlers/harness/get/screen.tsx @@ -1,9 +1,9 @@ import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { Harness, HarnessTool } from "@aws-sdk/client-bedrock-agentcore-control"; import type { ScreenProps } from "../../types"; -import { useCoreOpts } from "../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../utils"; import { credentialProviderTypeFromArn, parseArn, @@ -288,7 +288,7 @@ function useHarnessDetail({ ctx, core }: ScreenProps, harnessId: string | undefi // the API key providers behind the model and git-backed skills. Enter on a // linked row opens that resource's own hub in the region its ARN names. export function HarnessGetScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { harnessId } = useParams(); const { query: detail, region } = useHarnessDetail(props, harnessId); const harness = detail.data?.harness; diff --git a/src/handlers/harness/invoke/screen.tsx b/src/handlers/harness/invoke/screen.tsx index 838e85668..2726a4f36 100644 --- a/src/handlers/harness/invoke/screen.tsx +++ b/src/handlers/harness/invoke/screen.tsx @@ -1,10 +1,10 @@ import { useEffect, useRef, useState } from "react"; import { Box, Text, useInput, useWindowSize } from "ink"; import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams, useSearchParams } from "react-router"; +import { useParams, useSearchParams } from "react-router"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; import type { ScreenProps } from "../../types"; -import { coreOptsFromCtx } from "../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../utils"; import { HarnessPicker } from "../../../components/HarnessPicker"; import { HarnessEndpointPicker } from "../../../components/HarnessEndpointPicker"; import { Layout } from "../../../components/Layout"; @@ -37,7 +37,7 @@ const theme = darkTheme; export function HarnessInvokeScreen(props: ScreenProps) { const { harnessId, sessionId } = useParams(); const [search] = useSearchParams(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); if (!harnessId) { return ( @@ -95,9 +95,9 @@ export function HarnessChat({ variant, onBack, }: HarnessChatProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const { columns, rows } = useWindowSize(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const detail = useQuery({ queryKey: ["harness", opts.region, harnessId], diff --git a/src/handlers/harness/list/screen.tsx b/src/handlers/harness/list/screen.tsx index d31282677..9ae419b00 100644 --- a/src/handlers/harness/list/screen.tsx +++ b/src/handlers/harness/list/screen.tsx @@ -1,11 +1,11 @@ -import { useNavigate } from "react-router"; import type { ScreenProps } from "../../types"; import { HarnessPicker } from "../../../components/HarnessPicker"; +import { useRegionNavigate } from "../../utils"; // HarnessListScreen lists the caller's harnesses in a table; selecting one pushes // to HarnessGetScreen with the harness ID as a path value. export function HarnessListScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); return ( { @@ -41,8 +41,8 @@ interface EventPickerProps extends ScreenProps { } function EventPicker({ ctx, core, memoryId, actorId, sessionId }: EventPickerProps) { - const opts = coreOptsFromCtx(ctx); - const navigate = useNavigate(); + const opts = useCoreOpts(ctx); + const navigate = useRegionNavigate(); return ( { actorId: string; @@ -34,7 +34,7 @@ export function MemoryActorPicker({ onSelect, onBack, }: MemoryActorPickerProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return ( { expect(screen.lastFrame()).toContain("agentcore → memory → get → memory-1"); expect(screen.lastFrame()).not.toContain("→ json"); }); + + // A hub reached with ?region= (from project status or a harness's linked + // rows) fetches there; its actions must keep fetching there too. + test("a hub opened with ?region= carries the region into its actor list", async () => { + const core = new TestCoreClient(); + core.memory.setGetResponse(getMemoryOutput()); + core.memory.setListActorsResponse({ actorSummaries: [] }); + const screen = renderScreen("/agentcore/memory/get/memory-1?region=eu-west-1", { core }); + + await waitForText(screen.lastFrame, "list this Memory's events"); + expect(core.memory.calls.find((call) => call.method === "getMemory")?.args[2]).toMatchObject({ + region: "eu-west-1", + }); + + await screen.press("down"); + await screen.press("return"); + await waitForText(screen.lastFrame, "choose an actor to list sessions for"); + await waitFor(() => core.memory.calls.some((call) => call.method === "listActors")); + expect(core.memory.calls.find((call) => call.method === "listActors")?.args[1]).toMatchObject({ + region: "eu-west-1", + }); + }); }); diff --git a/src/handlers/memory/record/get/screen.tsx b/src/handlers/memory/record/get/screen.tsx index c3ed7444d..274ced5f1 100644 --- a/src/handlers/memory/record/get/screen.tsx +++ b/src/handlers/memory/record/get/screen.tsx @@ -2,10 +2,10 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; export function MemoryRecordGetScreen({ ctx, core }: ScreenProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const { memoryId, recordId } = useParams(); const detail = useQuery({ queryKey: ["memory-record", opts.region, memoryId, recordId], diff --git a/src/handlers/memory/record/list/screen.tsx b/src/handlers/memory/record/list/screen.tsx index 183139263..b191dcf2a 100644 --- a/src/handlers/memory/record/list/screen.tsx +++ b/src/handlers/memory/record/list/screen.tsx @@ -1,7 +1,7 @@ import type { MemoryContent, MemoryRecordSummary } from "@aws-sdk/client-bedrock-agentcore"; import { Box, Text, useInput } from "ink"; import { useState } from "react"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { FormRadioGroup, type FormRadioOption } from "../../../../components/FormRadioGroup"; import { FormTextInput } from "../../../../components/FormTextInput"; import { Layout } from "../../../../components/Layout"; @@ -11,7 +11,7 @@ import { formatTimestamp } from "../../../../components/formatTimestamp"; import { darkTheme } from "../../../../components/ui/_core"; import type { DataTableColumn } from "../../../../components/ui/data-table"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../../utils"; type RecordScopeKind = "namespace" | "namespace-path"; @@ -65,7 +65,7 @@ interface MemoryRecordScopeScreenProps { } function MemoryRecordScopeScreen({ memoryId }: MemoryRecordScopeScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const [focusedIndex, setFocusedIndex] = useState(0); const [scope, setScope] = useState(""); const [submitted, setSubmitted] = useState(false); @@ -158,8 +158,8 @@ interface MemoryRecordPickerProps extends ScreenProps { } function MemoryRecordPicker({ ctx, core, memoryId, scopeKind, scope }: MemoryRecordPickerProps) { - const opts = coreOptsFromCtx(ctx); - const navigate = useNavigate(); + const opts = useCoreOpts(ctx); + const navigate = useRegionNavigate(); return ( { await screen.press("escape"); await waitForText(screen.lastFrame, "manage an AgentCore project"); }); + + // A detail page opened from status fetches in the target's region, but its + // actions are routes of their own: without the override travelling with + // them the JSON view (and every list) would fetch in the ambient region and + // fail to find the resource. + test("the target region follows the Runtime into its detail JSON", async () => { + const value = core(); + const screen = renderStatus(value); + + await waitForGroup(screen); + await screen.press("down"); + await screen.press("return"); + await waitForText(screen.lastFrame, "show the full JSON definition"); + // invoke → shell → endpoints → versions → detail + for (let press = 0; press < 4; press++) await screen.press("down"); + await screen.press("return"); + + await waitForText(screen.lastFrame, "agentcore → runtime → get → " + RUNTIME_ID + " → json"); + await waitForText(screen.lastFrame, '"agentRuntimeId"'); + const fetches = value.runtime.calls.filter(({ method }) => method === "getRuntime"); + expect(fetches.length).toBeGreaterThanOrEqual(2); + for (const fetch of fetches) expect(fetch.args[1]).toMatchObject({ region: TARGET.region }); + }); + + test("the target region follows the Runtime into its endpoint list", async () => { + const value = core(); + value.runtime.setListEndpointsResponse({ runtimeEndpoints: [] }); + const screen = renderStatus(value); + + await waitForGroup(screen); + await screen.press("down"); + await screen.press("return"); + await waitForText(screen.lastFrame, "show the full JSON definition"); + await screen.press("down"); + await screen.press("down"); + await screen.press("return"); + + await waitForText(screen.lastFrame, "agentcore → runtime → endpoint → list → " + RUNTIME_ID); + await waitFor(() => + value.runtime.calls.some(({ method }) => method === "listRuntimeEndpoints"), + ); + const call = value.runtime.calls.find(({ method }) => method === "listRuntimeEndpoints")!; + expect(call.args[0]).toBe(RUNTIME_ID); + expect(call.args[3]).toMatchObject({ region: TARGET.region }); + }); + + test("the target region follows the Memory into its actor list", async () => { + const value = core(); + value.memory.setListActorsResponse({ actorSummaries: [] }); + const screen = renderStatus(value); + + await waitForGroup(screen); + await screen.press("down"); + await screen.press("down"); + await screen.press("return"); + await waitForText(screen.lastFrame, "list this Memory's events"); + await screen.press("down"); + await screen.press("return"); + + await waitForText(screen.lastFrame, "choose an actor to list sessions for"); + await waitFor(() => value.memory.calls.some(({ method }) => method === "listActors")); + const call = value.memory.calls.find(({ method }) => method === "listActors")!; + expect(call.args[0]).toMatchObject({ memoryId: MEMORY_ID }); + expect(call.args[1]).toMatchObject({ region: TARGET.region }); + }); }); diff --git a/src/handlers/runtime/endpoint/get/screen.tsx b/src/handlers/runtime/endpoint/get/screen.tsx index eec3e0ad2..6f14a20c0 100644 --- a/src/handlers/runtime/endpoint/get/screen.tsx +++ b/src/handlers/runtime/endpoint/get/screen.tsx @@ -1,16 +1,16 @@ import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import { ResourceDetailScreen } from "../../../../components/ResourceDetailScreen"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../../utils"; function useRuntimeEndpointDetail( { ctx, core }: ScreenProps, runtimeId: string | undefined, qualifier: string | undefined, ) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); return useQuery({ queryKey: ["runtime-endpoint", opts.region, runtimeId, qualifier], queryFn: () => core.runtime.getRuntimeEndpoint(runtimeId!, qualifier!, opts), @@ -24,7 +24,7 @@ function endpointPath(runtimeId: string, qualifier: string, suffix?: string): st } export function RuntimeGetEndpointScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { runtimeId, qualifier } = useParams(); const detail = useRuntimeEndpointDetail(props, runtimeId, qualifier); const endpoint = detail.data; diff --git a/src/handlers/runtime/endpoint/list/screen.tsx b/src/handlers/runtime/endpoint/list/screen.tsx index bdfa79865..809630fdd 100644 --- a/src/handlers/runtime/endpoint/list/screen.tsx +++ b/src/handlers/runtime/endpoint/list/screen.tsx @@ -1,10 +1,11 @@ -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { ScreenProps } from "../../../types"; import { RuntimeEndpointPicker } from "../../../../components/RuntimeEndpointPicker"; import { RuntimePicker } from "../../../../components/RuntimePicker"; +import { useRegionNavigate } from "../../../utils"; export function RuntimeListEndpointsScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { runtimeId } = useParams(); if (!runtimeId) { diff --git a/src/handlers/runtime/get/screen.tsx b/src/handlers/runtime/get/screen.tsx index 3b1862a6c..eb6a981c7 100644 --- a/src/handlers/runtime/get/screen.tsx +++ b/src/handlers/runtime/get/screen.tsx @@ -1,9 +1,9 @@ import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import { JsonDetail } from "../../../components/JsonDetail"; import { ResourceDetailScreen } from "../../../components/ResourceDetailScreen"; import type { ScreenProps } from "../../types"; -import { useCoreOpts } from "../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../utils"; const ACTIONS = [ { @@ -45,7 +45,7 @@ function useRuntimeDetail({ ctx, core }: ScreenProps, runtimeId: string | undefi } export function RuntimeGetScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { runtimeId } = useParams(); const detail = useRuntimeDetail(props, runtimeId); diff --git a/src/handlers/runtime/invoke/screen.tsx b/src/handlers/runtime/invoke/screen.tsx index 625edd52a..c1ef05d80 100644 --- a/src/handlers/runtime/invoke/screen.tsx +++ b/src/handlers/runtime/invoke/screen.tsx @@ -3,11 +3,11 @@ 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 { useLocation, useNavigate, useParams } from "react-router"; +import { useLocation, useParams } from "react-router"; import { ScrollView, type ScrollViewRef } from "ink-scroll-view"; import cliTruncate from "cli-truncate"; import type { ScreenProps } from "../../types"; -import { coreOptsFromCtx } from "../../utils"; +import { useCoreOpts, useRegionNavigate } from "../../utils"; import { Layout } from "../../../components/Layout"; import { MultilineInput } from "../../../components/MultilineInput"; import { RuntimeEndpointPicker } from "../../../components/RuntimeEndpointPicker"; @@ -95,7 +95,7 @@ function ErrorBlock({ details }: { details: ErrorDetails }) { export function RuntimeInvokeScreen(props: ScreenProps) { const { runtimeId, qualifier } = useParams(); const location = useLocation(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const launchContext = props.ctx.value(RuntimeInvokeLaunchContextKey); const initialContext = launchContext?.runtimeId === runtimeId ? launchContext : undefined; const returnOnEscape = (location.state as RuntimeInvokeLocationState | null)?.returnOnEscape; @@ -157,8 +157,8 @@ export function RuntimeInvokeConsole({ returnOnEscape, onBack, }: RuntimeInvokeConsoleProps) { - const opts = coreOptsFromCtx(ctx); - const navigate = useNavigate(); + const opts = useCoreOpts(ctx); + const navigate = useRegionNavigate(); const { columns, rows } = useWindowSize(); const [target, setTarget] = useState({ runtimeId, qualifier }); const [targetPicker, setTargetPicker] = useState(null); diff --git a/src/handlers/runtime/list/screen.tsx b/src/handlers/runtime/list/screen.tsx index 90a78493d..5037888b3 100644 --- a/src/handlers/runtime/list/screen.tsx +++ b/src/handlers/runtime/list/screen.tsx @@ -1,9 +1,9 @@ -import { useNavigate } from "react-router"; import type { ScreenProps } from "../../types"; import { RuntimePicker } from "../../../components/RuntimePicker"; +import { useRegionNavigate } from "../../utils"; export function RuntimeListScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); return ( { return frame.includes("agentcore → runtime → get → runtime-123") && !frame.includes("→ json"); }); }); + + // A hub reached with ?region= (from project status or a harness's linked + // rows) fetches there; its actions must keep fetching there too. + test("a hub opened with ?region= carries the region into its actions", async () => { + const core = new TestCoreClient(); + core.runtime.setGetResponse(getRuntimeResponse()); + core.runtime.setListEndpointsResponse({ runtimeEndpoints: [] }); + const r = renderScreen("/agentcore/runtime/get/runtime-123?region=eu-west-1", { core }); + + await waitForText(r.lastFrame, "show the full JSON definition"); + expect(core.runtime.calls.find((call) => call.method === "getRuntime")?.args[1]).toMatchObject({ + region: "eu-west-1", + }); + + await r.press("down"); + await r.press("down"); + await r.press("return"); + await waitForText(r.lastFrame, "agentcore → runtime → endpoint → list → runtime-123"); + await waitFor(() => core.runtime.calls.some((call) => call.method === "listRuntimeEndpoints")); + expect( + core.runtime.calls.find((call) => call.method === "listRuntimeEndpoints")?.args[3], + ).toMatchObject({ region: "eu-west-1" }); + + await r.press("escape"); + await waitForText(r.lastFrame, "show the full JSON definition"); + for (let index = 0; index < 4; index += 1) await r.press("down"); + await r.press("return"); + await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123 → json"); + await waitForText(r.lastFrame, '"agentRuntimeId"'); + const fetches = core.runtime.calls.filter((call) => call.method === "getRuntime"); + expect(fetches.length).toBeGreaterThanOrEqual(2); + for (const fetch of fetches) expect(fetch.args[1]).toMatchObject({ region: "eu-west-1" }); + }); }); diff --git a/src/handlers/runtime/shell/screen.tsx b/src/handlers/runtime/shell/screen.tsx index 6687b3c46..b16be5a9e 100644 --- a/src/handlers/runtime/shell/screen.tsx +++ b/src/handlers/runtime/shell/screen.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; import { useApp, useStderr, useStdin, useStdout } from "ink"; -import { useLocation, useNavigate, useParams } from "react-router"; +import { useLocation, useParams } from "react-router"; import { RuntimeEndpointPicker } from "../../../components/RuntimeEndpointPicker"; import { RuntimePicker } from "../../../components/RuntimePicker"; import { Spinner } from "../../../components/ui/spinner"; @@ -8,6 +8,7 @@ import { SilentCLIError } from "../../../errors"; import type { ScreenProps } from "../../types"; import { RuntimeShellLaunchContextKey } from "./launchContext"; import { runRuntimeShell } from "./operation"; +import { useRegionNavigate } from "../../utils"; type RuntimeShellLocationState = { returnOnEscape?: boolean; @@ -20,7 +21,7 @@ const shellPath = (...parts: string[]) => export function RuntimeShellScreen(props: ScreenProps) { const { runtimeId, qualifier } = useParams(); const location = useLocation(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const locationState = location.state as RuntimeShellLocationState | null; const returnOnEscape = locationState?.returnOnEscape; @@ -85,7 +86,7 @@ function RuntimeShellHandoff({ const { stdin } = useStdin(); const { stdout } = useStdout(); const { stderr } = useStderr(); - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const requested = useRef(false); const launchContext = ctx.value(RuntimeShellLaunchContextKey); const initialContext = launchContext?.runtimeId === runtimeId ? launchContext : undefined; diff --git a/src/handlers/runtime/version/get/screen.tsx b/src/handlers/runtime/version/get/screen.tsx index b74341947..9b17e6aa4 100644 --- a/src/handlers/runtime/version/get/screen.tsx +++ b/src/handlers/runtime/version/get/screen.tsx @@ -2,10 +2,10 @@ import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; import { JsonDetail } from "../../../../components/JsonDetail"; import type { ScreenProps } from "../../../types"; -import { coreOptsFromCtx } from "../../../utils"; +import { useCoreOpts } from "../../../utils"; export function RuntimeGetVersionScreen({ ctx, core }: ScreenProps) { - const opts = coreOptsFromCtx(ctx); + const opts = useCoreOpts(ctx); const { runtimeId, version } = useParams(); const detail = useQuery({ queryKey: ["runtime-version", opts.region, runtimeId, version], diff --git a/src/handlers/runtime/version/list/screen.tsx b/src/handlers/runtime/version/list/screen.tsx index bf9484063..8d5e09aeb 100644 --- a/src/handlers/runtime/version/list/screen.tsx +++ b/src/handlers/runtime/version/list/screen.tsx @@ -1,10 +1,11 @@ -import { useNavigate, useParams } from "react-router"; +import { useParams } from "react-router"; import type { ScreenProps } from "../../../types"; import { RuntimePicker } from "../../../../components/RuntimePicker"; import { RuntimeVersionPicker } from "../../../../components/RuntimeVersionPicker"; +import { useRegionNavigate } from "../../../utils"; export function RuntimeListVersionsScreen(props: ScreenProps) { - const navigate = useNavigate(); + const navigate = useRegionNavigate(); const { runtimeId } = useParams(); if (!runtimeId) { diff --git a/src/handlers/utils.test.tsx b/src/handlers/utils.test.tsx index 6b354f52b..8d5a47363 100644 --- a/src/handlers/utils.test.tsx +++ b/src/handlers/utils.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { parseJsonArrayFlag, parseJsonObjectFlag, parseTags } from "./utils"; +import { parseJsonArrayFlag, parseJsonObjectFlag, parseTags, withRegion } from "./utils"; describe("structured JSON flags", () => { test("parses object and array values", () => { @@ -65,3 +65,31 @@ describe("parseTags", () => { expect(() => parseTags(["noequals"])).toThrow("expected key=value"); }); }); + +describe("withRegion", () => { + test("appends the region as a query string", () => { + expect(withRegion("/agentcore/runtime/get/rt-1", "eu-west-1")).toBe( + "/agentcore/runtime/get/rt-1?region=eu-west-1", + ); + }); + + test("joins an existing query string", () => { + expect(withRegion("/agentcore/harness/exec/h-1?qualifier=PROD", "eu-west-1")).toBe( + "/agentcore/harness/exec/h-1?qualifier=PROD®ion=eu-west-1", + ); + }); + + test("keeps a region the route already names", () => { + expect(withRegion("/agentcore/memory/get/m-1?region=us-west-2", "eu-west-1")).toBe( + "/agentcore/memory/get/m-1?region=us-west-2", + ); + }); + + test("leaves the route alone without an override", () => { + expect(withRegion("/agentcore/runtime/get/rt-1", null)).toBe("/agentcore/runtime/get/rt-1"); + expect(withRegion("/agentcore/runtime/get/rt-1", undefined)).toBe( + "/agentcore/runtime/get/rt-1", + ); + expect(withRegion("/agentcore/runtime/get/rt-1", "")).toBe("/agentcore/runtime/get/rt-1"); + }); +}); diff --git a/src/handlers/utils.tsx b/src/handlers/utils.tsx index 55b79eb93..44fecb658 100644 --- a/src/handlers/utils.tsx +++ b/src/handlers/utils.tsx @@ -1,4 +1,5 @@ -import { useSearchParams } from "react-router"; +import { useCallback } from "react"; +import { useNavigate, useSearchParams, type NavigateOptions } from "react-router"; import type { Context } from "../router"; import type z from "zod"; import type { CoreOptions } from "../core/types"; @@ -32,6 +33,38 @@ export function useCoreOpts(ctx: Context): CoreOptions { return region ? { ...opts, region } : opts; } +// withRegion carries a region override onto a route as the ?region= query +// useCoreOpts reads on the other side. A route that already names a region +// keeps it (a linked resource's own region beats the current screen's), and +// without an override the route comes back unchanged. +export function withRegion(to: string, region: string | null | undefined): string { + if (!region) return to; + const [path = "", query = ""] = to.split("?", 2); + const params = new URLSearchParams(query); + if (params.has("region")) return to; + params.set("region", region); + return `${path}?${params.toString()}`; +} + +// useRegionNavigate is useNavigate for screens that may have been opened with +// a ?region= override. A hub reached through project status or a harness's +// linked rows fetches in that region, but its actions, the lists they open and +// the rows those lists forward to are all routes of their own — so string +// routes carry the override forward and the whole flow keeps fetching where +// the resource lives. History moves (navigate(-1)) pass through untouched. +export function useRegionNavigate(): (to: string | number, options?: NavigateOptions) => void { + const navigate = useNavigate(); + const [search] = useSearchParams(); + const region = search.get("region"); + return useCallback( + (to: string | number, options?: NavigateOptions) => { + if (typeof to === "number") navigate(to); + else navigate(withRegion(to, region), options); + }, + [navigate, region], + ); +} + // parseJsonFlag parses a flag's raw string as JSON, typed as the API structure // the flag mirrors. Structured API parameters (model/tools/memory/...) are // accepted as JSON documents rather than exploded into dozens of leaf flags;