From f09cc72dc842065b0599e0f2037f0eaea68302e0 Mon Sep 17 00:00:00 2001 From: Chirag Arora <76108151+ChiragArora31@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:35:54 +0530 Subject: [PATCH] feat(cli): hide terminal sessions in `ao session ls` by default (#1319) * feat(cli): hide terminal session rows by default in session ls - Filter with TERMINAL_STATUSES unless --include-terminated - Hint when completed sessions are hidden; docs/CLI.md updated Made-with: Cursor * fix(cli): keep session ls JSON inventory stable Address review concerns by preserving terminal sessions in `ao session ls --json` output while keeping terminal rows hidden by default in text output. Use the core terminal-session predicate, improve hidden-row messaging, and add regression tests for mixed/hinted and JSON behavior. Made-with: Cursor * fix(cli): resolve rebase conflicts with latest session ls behavior Align session command and CLI docs with the current mainline JSON contract (`data` + `meta.hiddenTerminatedCount`) while preserving terminal-filter defaults and updated regression coverage. Made-with: Cursor --- .changeset/cli-session-ls-terminated.md | 5 ++ .../cli/__tests__/commands/session.test.ts | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .changeset/cli-session-ls-terminated.md diff --git a/.changeset/cli-session-ls-terminated.md b/.changeset/cli-session-ls-terminated.md new file mode 100644 index 000000000..792882282 --- /dev/null +++ b/.changeset/cli-session-ls-terminated.md @@ -0,0 +1,5 @@ +--- +"@aoagents/ao-cli": patch +--- + +`ao session ls` hides terminal sessions in text output by default; use `--include-terminated` for the full text list. diff --git a/packages/cli/__tests__/commands/session.test.ts b/packages/cli/__tests__/commands/session.test.ts index 768c02590..57d30967a 100644 --- a/packages/cli/__tests__/commands/session.test.ts +++ b/packages/cli/__tests__/commands/session.test.ts @@ -370,6 +370,23 @@ describe("session ls", () => { }); }); + it("filters terminal sessions from JSON by default and reports hidden count", async () => { + writeFileSync(join(sessionsDir, "app-1"), "branch=main\nstatus=working\n"); + writeFileSync(join(sessionsDir, "app-done"), "branch=main\nstatus=merged\nactivity=exited\n"); + + mockTmux.mockResolvedValue(null); + mockGit.mockResolvedValue(null); + + await program.parseAsync(["node", "test", "session", "ls", "--json"]); + + const parsed = JSON.parse(String(consoleSpy.mock.calls[0][0])) as { + data: Array<{ id: string }>; + meta: { hiddenTerminatedCount: number }; + }; + expect(parsed.data.map((entry) => entry.id)).toEqual(["app-1"]); + expect(parsed.meta.hiddenTerminatedCount).toBe(1); + }); + it("marks metadata-based orchestrators correctly in JSON output", async () => { writeFileSync( join(sessionsDir, "app-control"), @@ -573,6 +590,71 @@ describe("session ls", () => { expect(parsed.data.map((e: { id: string }) => e.id)).toEqual(["app-1"]); expect(parsed.meta.hiddenTerminatedCount).toBe(3); }); + + it("hides terminal-status sessions by default", async () => { + writeFileSync(join(sessionsDir, "app-1"), "branch=main\nstatus=working\n"); + writeFileSync(join(sessionsDir, "app-done"), "branch=main\nstatus=merged\n"); + + mockTmux.mockImplementation(async (...args: string[]) => { + if (args[0] === "list-sessions") return "app-1\napp-done"; + if (args[0] === "display-message") { + return String(Math.floor(Date.now() / 1000) - 60); + } + return null; + }); + mockGit.mockResolvedValue(null); + + await program.parseAsync(["node", "test", "session", "ls"]); + + const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n"); + expect(output).toContain("app-1"); + expect(output).not.toContain("app-done"); + }); + + it("lists terminal sessions when --include-terminated is set", async () => { + writeFileSync(join(sessionsDir, "app-done"), "branch=main\nstatus=merged\n"); + + mockTmux.mockImplementation(async (...args: string[]) => { + if (args[0] === "list-sessions") return "app-done"; + return null; + }); + mockGit.mockResolvedValue(null); + + await program.parseAsync(["node", "test", "session", "ls", "--include-terminated"]); + + const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n"); + expect(output).toContain("app-done"); + }); + + it("prints a hint when only terminal sessions exist", async () => { + writeFileSync(join(sessionsDir, "app-done"), "branch=main\nstatus=merged\n"); + + mockTmux.mockImplementation(async (...args: string[]) => { + if (args[0] === "list-sessions") return "app-done"; + return null; + }); + + await program.parseAsync(["node", "test", "session", "ls"]); + + const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n"); + expect(output).toContain("--include-terminated"); + expect(output).not.toContain("app-done"); + }); + + it("prints a hint when terminal sessions are hidden alongside active ones", async () => { + writeFileSync(join(sessionsDir, "app-1"), "branch=main\nstatus=working\n"); + writeFileSync(join(sessionsDir, "app-done"), "branch=main\nstatus=merged\n"); + + mockTmux.mockResolvedValue(null); + mockGit.mockResolvedValue(null); + + await program.parseAsync(["node", "test", "session", "ls"]); + + const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n"); + expect(output).toContain("app-1"); + expect(output).toContain("terminated session"); + expect(output).toContain("--include-terminated"); + }); }); describe("session kill", () => {