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
16 changes: 9 additions & 7 deletions src/poly/cli_commands/branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,8 +444,8 @@ def add_arguments(cls, subparsers: _SubParsersAction[ArgumentParser], parents: P
branch_history_parser.add_argument(
"--limit",
type=int,
default=10,
help="Number of history entries to show. Defaults to 10.",
default=None,
help="Maximum number of history entries to show. Shows all by default.",
)
branch_history_parser.set_defaults(branch_subcommand="history")

Expand Down Expand Up @@ -1575,10 +1575,10 @@ def branch_history(
base_path: str,
branch_name: Optional[str] = None,
output_json: bool = False,
limit: int = 10,
limit: Optional[int] = None,
) -> None:
"""Show the history of a branch in the Agent Studio project."""
from poly.output.console import plain, print_branch_history, warning
from poly.output.console import paged_output, plain, print_branch_history, warning

project = load_project(base_path, output_json=output_json)

Expand Down Expand Up @@ -1607,7 +1607,8 @@ def branch_history(
return

history = project.get_branch_history(branch_id)
history = history[:limit]
if limit is not None:
history = history[:limit]

if output_json:
json_print({"branch_name": branch_name, "branch_id": branch_id, "history": history})
Expand All @@ -1617,8 +1618,9 @@ def branch_history(
plain(f"[muted]No history found for branch '{branch_name}'.[/muted]")
return

plain(f"History for branch '{branch_name}':")
print_branch_history(history)
with paged_output():
plain(f"History for branch '{branch_name}':")
print_branch_history(history)

@classmethod
def branch_rename(
Expand Down
5 changes: 3 additions & 2 deletions src/poly/cli_commands/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def conversations_list(
offset: Number of conversations to skip.
output_json: If True, emit machine-readable JSON.
"""
from poly.output.console import info, print_conversations
from poly.output.console import info, paged_output, print_conversations

project = load_project(base_path, output_json=output_json)
result = AgentStudioInterface.list_conversations(
Expand All @@ -176,7 +176,8 @@ def conversations_list(
if not conversations:
info("No conversations found.")
return
print_conversations(conversations, url_builder=project.get_conversation_url)
with paged_output():
print_conversations(conversations, url_builder=project.get_conversation_url)

@classmethod
def conversations_get(
Expand Down
27 changes: 15 additions & 12 deletions src/poly/cli_commands/deployments.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ def add_arguments(cls, subparsers: _SubParsersAction[ArgumentParser], parents: P
deployment_list_parser.add_argument(
"--limit",
type=int,
default=10,
help="Number of versions to show. Defaults to 10.",
default=None,
help="Maximum number of versions to show. Shows all by default.",
)
deployment_list_parser.add_argument(
"--offset",
Expand Down Expand Up @@ -487,29 +487,29 @@ def deployments_list(
cls,
base_path: str,
environment: Optional[str] = None,
limit: int = 10,
limit: Optional[int] = None,
offset: int = 0,
version_hash: str = None,
output_json: bool = False,
details: bool = False,
) -> None:
"""List deployment history for the project.

By default shows the 10 most recent deployments for live on projects using
simplified deployments, and for sandbox otherwise. Pass version_hash to start
the listing from a specific version. Use details for full per-deployment metadata.
By default shows all deployments for live on projects using simplified
deployments, and for sandbox otherwise. Pass version_hash to start the listing
from a specific version. Use details for full per-deployment metadata.

Args:
base_path: Base path for the project.
environment: Environment to query — sandbox, pre-release, or live. Defaults
to the environment holding the project's deployments.
limit: Maximum number of versions to show.
limit: Maximum number of versions to show. Shows all by default.
offset: Number of versions to skip before showing results.
version_hash: Start listing from this version hash (overrides offset).
output_json: If True, print result as JSON instead of rich text.
details: If True, print full metadata for each deployment.
"""
from poly.output.console import error, print_deployments
from poly.output.console import error, paged_output, print_deployments

project = load_project(base_path, output_json=output_json)
if environment is None:
Expand All @@ -535,15 +535,17 @@ def deployments_list(
return
offset = version_idx

versions = versions[offset : offset + limit]
end = offset + limit if limit is not None else None
versions = versions[offset:end]
if output_json:
json_output = {
"versions": versions,
"active_deployment_hashes": active_deployment_hashes,
}
json_print(json_output)
else:
print_deployments(versions, active_deployment_hashes, details=details)
with paged_output():
print_deployments(versions, active_deployment_hashes, details=details)

@classmethod
def deployments_show(
Expand Down Expand Up @@ -1006,15 +1008,16 @@ def ab_test_list(
output_json: bool = False,
) -> None:
"""List A/B tests for the project."""
from poly.output.console import print_ab_tests
from poly.output.console import paged_output, print_ab_tests

project = load_project(base_path, output_json=output_json)
ab_tests = project.list_ab_tests(limit=limit)
if output_json:
json_print({"success": True, "ab_tests": ab_tests})
else:
dep_map = cls._fetch_deployment_map(project) if ab_tests else {}
print_ab_tests(ab_tests, deployments=dep_map)
with paged_output():
print_ab_tests(ab_tests, deployments=dep_map)

@classmethod
def ab_test_active(
Expand Down
5 changes: 3 additions & 2 deletions src/poly/cli_commands/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def testing_list(
output_json: bool = False,
) -> None:
"""List test runs."""
from poly.output.console import print_test_run_list
from poly.output.console import paged_output, print_test_run_list

project = load_project(base_path)
result = project.list_test_runs(limit=limit, offset=offset)
Expand All @@ -261,7 +261,8 @@ def testing_list(
json_print({"success": True, "test_runs": result})
return

print_test_run_list(result)
with paged_output():
print_test_run_list(result)

@classmethod
def testing_show(
Expand Down
38 changes: 35 additions & 3 deletions src/poly/output/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
import logging
import os
import sys
from collections.abc import Callable
from collections.abc import Callable, Iterator
from contextlib import contextmanager
from datetime import datetime
from typing import Any
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from rich import box
from rich.console import Console, Group
from rich.live import Live
from rich.pager import Pager, SystemPager
from rich.panel import Panel
from rich.spinner import Spinner
from rich.syntax import Syntax
Expand Down Expand Up @@ -55,6 +57,36 @@ def set_verbose(verbose: bool) -> None:
_verbose = verbose


class _OverflowPager(Pager):
"""Pager that only engages when the content overflows the screen.

Rich's default pager sends everything to ``less``, so even a three-row table
would open the pager and demand ``q``. git avoids that with ``less -F``
(quit-if-one-screen); deciding it here instead means it also holds for a
reader whose ``PAGER`` is ``bat`` or ``more``, or whose ``LESS`` has no
``F``. Spawning the pager, colour passthrough, ctrl-c and quitting early are
all left to Rich and pydoc, which already handle them.
"""

def show(self, content: str) -> None:
"""Write content straight out if it fits, otherwise hand it to the pager."""
# Rich has already wrapped to the console width, so newlines are rows.
if content.count("\n") < console.size.height:
console.file.write(content)
else:
SystemPager().show(content)


@contextmanager
def paged_output(enabled: bool = True) -> Iterator[None]:
"""Pipe output through the system pager when enabled and stdout is a TTY."""
if enabled and console.is_terminal:
with console.pager(pager=_OverflowPager(), styles=True):
yield
else:
yield


# ── Helpers ──────────────────────────────────────────────────────────


Expand Down Expand Up @@ -307,7 +339,7 @@ def print_branch_history(commits: list[dict[str, Any]]) -> None:
console.print("[muted]No commits found for this branch.[/muted]")
return

table = Table(box=None, show_header=False, header_style="bold", padding=(0, 1))
table = Table(box=None, show_header=True, header_style="bold", padding=(0, 1))
table.add_column("Merged At", no_wrap=True)
table.add_column("Branch", no_wrap=True)
table.add_column("Merged By", no_wrap=True)
Expand Down Expand Up @@ -699,7 +731,7 @@ def print_deployments(
if not details:
table = Table(
box=None,
show_header=False,
show_header=True,
header_style="bold",
padding=(0, 1),
)
Expand Down
24 changes: 17 additions & 7 deletions src/poly/tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1979,17 +1979,27 @@ def test_no_versions_calls_error(self, mock_error):
self.assertIn("No versions found", mock_error.call_args[0][0])

@patch("poly.output.console.print_deployments")
def test_default_call_shows_first_ten(self, mock_print_dep):
"""Default call (no hash, no json) displays the first 10 versions."""
def test_default_call_shows_all(self, mock_print_dep):
"""Default call (no hash, no json, no limit) displays all versions."""
self.proj.get_deployments.return_value = (self.versions, self.active_hashes)

DeploymentsCommand.deployments_list(TEST_DIR)

mock_print_dep.assert_called_once()
displayed_versions = mock_print_dep.call_args[0][0]
self.assertEqual(len(displayed_versions), 10)
self.assertEqual(len(displayed_versions), 15)
self.assertEqual(displayed_versions[0]["name"], "v0")

@patch("poly.output.console.print_deployments")
def test_explicit_limit(self, mock_print_dep):
"""--limit caps the number of displayed versions."""
self.proj.get_deployments.return_value = (self.versions, self.active_hashes)

DeploymentsCommand.deployments_list(TEST_DIR, limit=5)

displayed_versions = mock_print_dep.call_args[0][0]
self.assertEqual(len(displayed_versions), 5)

@patch("poly.cli_commands.deployments.json_print")
def test_output_json_calls_json_print(self, mock_json_print):
"""print_deployments with output_json=True calls json_print."""
Expand All @@ -2001,7 +2011,7 @@ def test_output_json_calls_json_print(self, mock_json_print):
output = mock_json_print.call_args[0][0]
self.assertIn("versions", output)
self.assertIn("active_deployment_hashes", output)
self.assertEqual(len(output["versions"]), 10)
self.assertEqual(len(output["versions"]), 15)

@patch("poly.output.console.print_deployments")
def test_hash_sets_offset(self, mock_print_dep):
Expand Down Expand Up @@ -3632,8 +3642,8 @@ def test_limit_truncates_history(self, mock_plain, mock_print_history):

@patch("poly.output.console.print_branch_history")
@patch("poly.output.console.plain")
def test_default_limit_is_10(self, mock_plain, mock_print_history):
"""Without --limit, history is capped at 10 entries."""
def test_no_limit_shows_all(self, mock_plain, mock_print_history):
"""Without --limit, all history entries are shown."""
self.proj.get_branch_history.return_value = [
{"mergedAt": f"2026-07-{i:02d}"} for i in range(1, 21)
]
Expand All @@ -3642,7 +3652,7 @@ def test_default_limit_is_10(self, mock_plain, mock_print_history):

mock_print_history.assert_called_once()
printed = mock_print_history.call_args[0][0]
self.assertEqual(len(printed), 10)
self.assertEqual(len(printed), 20)

@patch("poly.cli_commands.branch.json_print")
def test_limit_applies_to_json_output(self, mock_json):
Expand Down
Loading
Loading