Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ agentplay agents show which agents were found on this machine
| `--agents claude-code,codex` | build, view, list | limit to these agents |
| `--rebuild` | build, view | rebuild even unchanged sessions |
| `--path-mode dirs_only` | build, view | see [Privacy](#privacy) |
| `--limit N` | build, view, list | newest N sessions |
| `--limit N` | build, view, list | newest N sessions (for list: after `--sort`) |
| `--sort cost` | list | order before `--limit`: `recent` (default), `cost`, `duration`, `calls`, `errors` |
| `--no-titles` | build, view | don't keep local titles, and clear any already stored |
| `--port 7317`, `--no-open`, `--no-build` | view | server options |
| `--json` | build, list | machine-readable output |
Expand Down
29 changes: 28 additions & 1 deletion python/src/agentplay/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,31 @@ def cmd_view(args: argparse.Namespace) -> int:


# --------------------------------------------------------------------------- list
_LIST_SORT_FIELDS = {
"recent": "startedAt",
"cost": "costUsd",
"duration": "durationMs",
"calls": "calls",
"errors": "errors",
}


def _sort_list_rows(rows: list[dict], sort: str) -> list[dict]:
"""Sort before --limit. Highest / newest first; rows missing the field last."""
field = _LIST_SORT_FIELDS[sort]
if field == "startedAt":
present = [r for r in rows if r.get("startedAt")]
missing = [r for r in rows if not r.get("startedAt")]
present.sort(key=lambda r: r["startedAt"], reverse=True)
return present + missing

def key(row: dict):
value = row.get(field)
return (value is None, 0 if value is None else -value)

return sorted(rows, key=key)


def cmd_list(args: argparse.Namespace) -> int:
st = Store()
agents = _agents(args.agents)
Expand All @@ -261,7 +286,7 @@ def cmd_list(args: argparse.Namespace) -> int:
if changed < since.timestamp():
continue
rows.append(row)
rows = rows[: args.limit or 30]
rows = _sort_list_rows(rows, args.sort)[: args.limit or 30]
if args.json:
print(json.dumps(rows, indent=2))
return 0
Expand Down Expand Up @@ -544,6 +569,8 @@ def parser() -> argparse.ArgumentParser:
ls = sub.add_parser("list", parents=[common], help="list built sessions")
ls.add_argument("--agents", help=f"comma-separated subset of: {', '.join(discover.AGENTS)}")
ls.add_argument("--since", help="only sessions changed since (30d, 12h, 2w, YYYY-MM-DD); missing modification times use start time")
ls.add_argument("--sort", default="recent", choices=list(_LIST_SORT_FIELDS),
help="order before --limit (default: recent)")
ls.add_argument("--limit", type=int)
ls.add_argument("--json", action="store_true")
ls.set_defaults(fn=cmd_list)
Expand Down
31 changes: 31 additions & 0 deletions python/tests/test_cli_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,37 @@ def test_list_filters_independently(list_store, capsys, flags, expected):
assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == expected


def test_list_sort_applies_before_limit(monkeypatch, tmp_path, capsys):
monkeypatch.setenv("AGENTPLAY_HOME", str(tmp_path))
monkeypatch.setattr(cli, "_TTY", False)
st = cli.Store()
for sid, started, cost, duration, calls, errors in [
("cheap", "2026-10-01T00:00:00Z", 0.1, 60_000, 1, 0),
("mid", "2026-10-02T00:00:00Z", 1.0, 120_000, 5, 1),
("pricey", "2026-10-03T00:00:00Z", 9.0, 30_000, 2, 0),
("long", "2026-09-01T00:00:00Z", 0.5, 600_000, 3, 0),
("missing", "2026-10-04T00:00:00Z", None, None, None, None),
]:
st.index[sid] = {
"sessionId": sid, "agentTool": "codex", "startedAt": started,
"costUsd": cost, "durationMs": duration, "calls": calls, "errors": errors,
}
st.save_index()
assert cli.main(["list", "--json", "--sort", "cost", "--limit", "3"]) == 0
assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["pricey", "mid", "long"]
assert cli.main(["list", "--json", "--sort", "duration", "--limit", "2"]) == 0
assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["long", "mid"]
assert cli.main(["list", "--json", "--sort", "calls", "--limit", "2"]) == 0
assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["mid", "long"]
assert cli.main(["list", "--json", "--sort", "errors", "--limit", "2"]) == 0
assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["mid", "pricey"]
# Missing numeric fields sort last; recent still newest-first.
assert cli.main(["list", "--json", "--sort", "cost"]) == 0
assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)][-1] == "missing"
assert cli.main(["list", "--json", "--sort", "recent", "--limit", "2"]) == 0
assert [r["sessionId"] for r in json.loads(capsys.readouterr().out)] == ["missing", "pricey"]


@pytest.mark.parametrize(("flags", "message"), [
(["--agents", "unknown"], "unknown agent"),
(["--since", "yesterday"], "can't read --since"),
Expand Down
Loading