Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions src/components/CliOnlyScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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();
Expand Down
49 changes: 49 additions & 0 deletions src/components/RouterScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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 });
Expand Down
5 changes: 4 additions & 1 deletion src/components/RouterScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading