From 5be0da38412edec090c557733c05bc954a6ffef6 Mon Sep 17 00:00:00 2001 From: gitikavj Date: Thu, 3 Sep 2026 22:20:29 +0000 Subject: [PATCH] fix(tui): report the not-found guidance when project invoke opens outside a project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The picker checked its loading branch before its error branch, so a failed project resolution left it spinning on "Resolving project…" forever. Route it through ProjectGate like project build and deploy, which prints the same message the commands do. --- .../project/invoke/invoke.screen.test.tsx | 37 ++++++++++- src/handlers/project/invoke/screen.tsx | 66 ++++++++++++------- 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/src/handlers/project/invoke/invoke.screen.test.tsx b/src/handlers/project/invoke/invoke.screen.test.tsx index 30b6a49dc..753b3218f 100644 --- a/src/handlers/project/invoke/invoke.screen.test.tsx +++ b/src/handlers/project/invoke/invoke.screen.test.tsx @@ -4,12 +4,31 @@ import type { GetAgentRuntimeResponse, GetHarnessResponse, } from "@aws-sdk/client-bedrock-agentcore-control"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { ProjectSpecSchema } from "../../../projectSchemas/project"; import { ProjectKey } from "../../../router"; -import { cleanupScreens, renderScreen, TestCoreClient, waitForText } from "../../../testing"; +import { + cleanupScreens, + flatFrame, + renderScreen, + TestCoreClient, + waitForFlatText, + waitForText, +} from "../../../testing"; import type { Project, ResolvedDeployedResource } from "../types"; +const originalCwd = process.cwd(); +const tempDirectories: string[] = []; + afterEach(cleanupScreens); +afterEach(async () => { + process.chdir(originalCwd); + await Promise.all( + tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); const project: Project = { name: "orders", @@ -114,6 +133,22 @@ describe("project invoke picker", () => { await waitForText(screen.lastFrame, "manage an AgentCore project"); }); + test("reports the CLI's own guidance outside a project", async () => { + const directory = await mkdtemp(join(tmpdir(), "agentcore-no-project-")); + tempDirectories.push(directory); + process.chdir(directory); + const screen = renderScreen("/agentcore/project/invoke", { core: core() }); + + await waitForFlatText(screen.lastFrame, "No AgentCore project found"); + const frame = flatFrame(screen.lastFrame); + expect(frame).toContain(directory); + expect(frame).toContain("agentcore project create"); + expect(frame).not.toContain("Resolving project"); + // esc is a way off the error, not just ctl+c. + await screen.press("escape"); + await waitForText(screen.lastFrame, "manage an AgentCore project"); + }); + test("resolves the enclosing project when opened from the project menu", async () => { const value = core(); value.projectManager.resolve = async () => project; diff --git a/src/handlers/project/invoke/screen.tsx b/src/handlers/project/invoke/screen.tsx index 07a509876..c4a0246e4 100644 --- a/src/handlers/project/invoke/screen.tsx +++ b/src/handlers/project/invoke/screen.tsx @@ -10,8 +10,8 @@ import { HarnessChat } from "../../harness/invoke/screen"; import { RegionKey } from "../../keys"; import { RuntimeInvokeConsole } from "../../runtime/invoke/screen"; import type { ScreenProps } from "../../types"; -import type { ResolvedDeployedResources } from "../types"; -import { useProject } from "../ProjectGate"; +import type { Project, ResolvedDeployedResources } from "../types"; +import { ProjectGate } from "../ProjectGate"; type ProjectInvokableRow = Record & { resourceType: "runtime" | "harness"; @@ -33,18 +33,40 @@ type Destination = | { resourceType: "runtime"; id: string; ctx: Context; qualifier?: string } | { resourceType: "harness"; id: string; ctx: Context }; +const BREADCRUMB = ["agentcore", "project", "invoke"]; +const PROJECT_MENU = "/agentcore/project"; + +// The project comes from the launch context when a project command opened the +// TUI, and is resolved from the cwd otherwise — the gate reports the CLI's own +// not-found guidance when there is none, rather than spinning forever. export function ProjectInvokePickerScreen({ ctx, core }: ScreenProps) { const navigate = useNavigate(); - // The project comes from the launch context when a project command opened - // the TUI, and is resolved from the cwd otherwise. - const { data: project, error: projectError } = useProject(core, ctx.value(ProjectKey)); + return ( + navigate(PROJECT_MENU)} + > + {(project) => } + + ); +} + +function ProjectInvokePicker({ + ctx, + core, + project, +}: ScreenProps & { + project: Project; +}) { + const navigate = useNavigate(); const [deployed, setDeployed] = useState(); const [destination, setDestination] = useState(); - const [deployedError, setDeployedError] = useState(); - const error = projectError?.message ?? deployedError; + const [error, setError] = useState(); useEffect(() => { - if (!project) return; let active = true; void core.projectManager .resolveDeployedResources(project, { target: "default" }) @@ -52,7 +74,7 @@ export function ProjectInvokePickerScreen({ ctx, core }: ScreenProps) { if (active) setDeployed(resolved); }) .catch((cause: unknown) => { - if (active) setDeployedError(cause instanceof Error ? cause.message : String(cause)); + if (active) setError(cause instanceof Error ? cause.message : String(cause)); }); return () => { active = false; @@ -63,7 +85,7 @@ export function ProjectInvokePickerScreen({ ctx, core }: ScreenProps) { () => (deployed?.resources ?? []).map((resource) => { if (resource.resourceType === "runtime") { - const configured = project?.spec.runtimes.find(({ name }) => name === resource.name); + const configured = project.spec.runtimes.find(({ name }) => name === resource.name); return { ...resource, type: "Runtime" as const, @@ -71,7 +93,7 @@ export function ProjectInvokePickerScreen({ ctx, core }: ScreenProps) { source: configured?.codeLocation ?? "-", }; } - const configured = project?.spec.harnesses.find(({ name }) => name === resource.name); + const configured = project.spec.harnesses.find(({ name }) => name === resource.name); return { ...resource, type: "Harness" as const, @@ -91,9 +113,9 @@ export function ProjectInvokePickerScreen({ ctx, core }: ScreenProps) { }); }; - const goBack = () => navigate("/agentcore/project"); + const goBack = () => navigate(PROJECT_MENU); useInput((_input, key) => { - if (key.escape && (!project || !deployed || error !== undefined)) goBack(); + if (key.escape && (!deployed || error !== undefined)) goBack(); }); if (destination?.resourceType === "runtime") { @@ -133,39 +155,39 @@ export function ProjectInvokePickerScreen({ ctx, core }: ScreenProps) { ); } - if (!project || (!deployed && !error)) { + if (error !== undefined) { return ( - + ✗ {error} ); } - if (error) { + if (!deployed) { return ( - {error} + ); } return (