diff --git a/src/components/CliOnlyScreen.test.tsx b/src/components/CliOnlyScreen.test.tsx index 0812a4118..65cc00ea7 100644 --- a/src/components/CliOnlyScreen.test.tsx +++ b/src/components/CliOnlyScreen.test.tsx @@ -39,7 +39,7 @@ function cliOnlyCommands( const CLI_ONLY = cliOnlyCommands(); -describe("menus list command-line-only subcommands below a divider", () => { +describe("menus separate mixed command groups with a divider", () => { test("the root menu", async () => { const r = renderScreen("/agentcore", { withContext: inProjectContext }); @@ -77,13 +77,14 @@ describe("menus list command-line-only subcommands below a divider", () => { r.unmount(); }); - test("a menu whose every subcommand is command line only", async () => { + test("a menu whose every subcommand is command line only has no redundant divider", async () => { const r = renderScreen("/agentcore/eval/ondemand"); - await waitFor(() => hasCliDivider(r.lastFrame()!)); + await waitForText(r.lastFrame, "❯ evaluate"); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); expect(menuEntries(r.lastFrame()!)).toEqual({ - screens: [], - cliOnly: ["evaluate", "simulate"], + screens: ["evaluate", "simulate"], + cliOnly: [], }); r.unmount(); }); @@ -133,10 +134,12 @@ describe("every command-line-only command opens on screen", () => { const parent = command.parent!; if (command.commands.length > 0) { - // A group opens its own menu, with every child under the divider. + // A CLI-only group opens its own menu without a redundant separator. await waitForText(r.lastFrame, path.join(" → ")); - await waitFor(() => hasCliDivider(r.lastFrame()!)); - expect(menuEntries(r.lastFrame()!).screens).toEqual([]); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); + expect(menuEntries(r.lastFrame()!).screens).toEqual( + command.commands.map((child) => child.name()), + ); } else { await waitForText(r.lastFrame, "this command runs from the command line"); const help = command.createHelp(); diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index 7af18b3b4..f81a30231 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -64,6 +64,28 @@ function menuGroups(frame: string): { title: string | undefined; names: string[] // rendered frames — behavior a user would see, not internal state. describe("menu rendering", () => { + test.each([ + "/agentcore/log", + "/agentcore/traces", + "/agentcore/harness/traces", + "/agentcore/runtime/traces", + "/agentcore/payment", + "/agentcore/payment/connector", + "/agentcore/payment/manager", + "/agentcore/payment/session", + "/agentcore/payment/instrument", + ])("%s has no extra divider above a CLI-only menu", async (path) => { + const r = renderScreen(path, { withContext: inProjectContext }); + await waitForText(r.lastFrame, "type to choose a command"); + + const frame = r.lastFrame()!; + const lines = frame.split("\n"); + const filterRow = lines.findIndex((line) => /^\s*\/ /.test(line)); + expect(lines[filterRow + 2]).toMatch(/^\s*❯ /); + expect(hasCliDivider(frame)).toBe(false); + r.unmount(); + }); + test("CLI-only command names use the same white and focused colors as other commands", () => { // A separate process keeps ANSI enabled without changing the plain-text screen tests. const result = spawnSync( @@ -316,6 +338,21 @@ describe("narrow terminals", () => { }); describe("filtering", () => { + test("removes the CLI-only divider when filtering leaves only that group and restores it", async () => { + const r = renderScreen("/agentcore"); + await waitForText(r.lastFrame, "❯ create"); + expect(hasCliDivider(r.lastFrame()!)).toBe(true); + + await r.write("feedback"); + await waitForText(r.lastFrame, "❯ feedback"); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); + + for (const _ of "feedback") await r.press("backspace"); + await waitForText(r.lastFrame, "❯ create"); + expect(hasCliDivider(r.lastFrame()!)).toBe(true); + r.unmount(); + }); + test("typing narrows the options to matches", async () => { const r = renderScreen("/agentcore/harness"); await waitForText(r.lastFrame, "list"); @@ -433,6 +470,18 @@ describe("navigation", () => { describe("short terminals", () => { const ROWS = 15; + test("does not reserve a row for a redundant CLI-only divider", async () => { + const r = renderScreen("/agentcore/log", { withContext: inProjectContext }); + await waitForText(r.lastFrame, "❯ runtime"); + await r.resize(100, 8); + + expect(r.lastFrame()).toContain("❯ runtime"); + expect(r.lastFrame()).toContain("harness"); + expect(r.lastFrame()).not.toContain("more"); + expect(hasCliDivider(r.lastFrame()!)).toBe(false); + r.unmount(); + }); + // fullMenu renders the root menu at the default height, where every option fits. async function fullMenu() { const r = renderScreen("/agentcore", { withContext: inProjectContext }); diff --git a/src/components/RouterScreen.tsx b/src/components/RouterScreen.tsx index 47bea52c2..aab8c90fb 100644 --- a/src/components/RouterScreen.tsx +++ b/src/components/RouterScreen.tsx @@ -289,7 +289,10 @@ function CommandMenuBody({ query, onQueryChange, }: CommandMenuBodyProps) { - const sections = useMemo(() => filtered.map((option) => option.section), [filtered]); + const sections = useMemo(() => { + const onlyCli = filtered.every((option) => option.section === CLI_ONLY_SECTION); + return filtered.map((option) => (onlyCli ? undefined : option.section)); + }, [filtered]); const menuHeight = Math.max(0, contentRows - FILTER_ROWS - alertRows(alert, columns)); const windowStart = Math.max(0, highlight - Math.floor(menuHeight / 2)); const view = scrollWindow({