feat(list): page list/find like git (interactive-only) + higher TTY limit#6
Conversation
…imit Adds the pagination half of the ROADMAP items "Commands should work OUTSIDE a repo; task list groups by repo/project" + "task list: fallback to all-tasks + pagination + session-vs-all messaging" (the outside-repo/grouping/session fallback shipped in #5; this is the missing pager + interactive-limit refinement "task list pager: interactive-only + higher limit there"). Behavior (git's pager model): - Human (non-json) list/find output pages through less ONLY when stdout is an interactive TTY; piped/scripted output is plain text (scriptable). Short output that fits one screen prints directly (less -F). - Result cap follows interactivity: 100 in a terminal (the pager scrolls), 30 when piped, unless an explicit count is given (which always wins). - Opt out with the no-pager flag, NO_PAGER (any non-empty value), or an empty PAGER/TASK_PAGER (git's "cat, don't page"). Pager resolves TASK_PAGER -> PAGER -> less -> more; LESS defaults to FRX. - JSON output is never paged (machine-readable stays un-paged, un-decorated). Implementation: - New tasklib/pager.py: pure should_page decision (TTY + not-opted-out + a pager resolves) and a defensive page() that pipes through the pager and falls back to a direct write on any failure. Pipe is forced utf-8 (ticket titles can be non-ASCII; a locale-encoding crash would otherwise escape). stdin is always closed in finally so the pager sees EOF. - cli._emit_list is the single call site; _format_tickets/_format_groups now return strings (were direct prints) so output can be routed; JSON paths print straight to stdout via _print_tickets_json/_print_groups_json. - _effective_limit picks the cap by interactivity; the count default is None so an explicit value is distinguishable. Tests: tasklib/pager.py decision + routing (real child-process pager via TASK_PAGER, utf-8, broken-pipe/non-zero-exit/missing-binary fallbacks, LESS=FRX seeding, empty/whitespace pager, closed-stream isatty); cmd_list/cmd_find wiring (interactive paging, piped-not-paged-even-with-pager-configured, no-pager, NO_PAGER, JSON never paged, explicit count flows into the backend, grouped paging on a TTY). Docs: README pagination section, AGENTS.md rule, install.py SKILL_MD. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 606633d431
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| fake_pager.write_text(f'#!/bin/sh\ncat > "{sink}"\n', encoding="utf-8") | ||
| fake_pager.chmod(0o755) | ||
| out = _Stream(tty=True) | ||
| body = "#1 [todo] Починить заголовок — café ☕" |
There was a problem hiding this comment.
Remove Cyrillic from the test fixture
AGENTS.md sets the repo-wide style rule to use English-only code/comments/docs and explicitly says no Cyrillic; this newly added fixture introduces Cyrillic text. The encoding coverage can stay intact by using a Latin non-ASCII sample such as accents plus emoji, without violating the documented repository guideline.
Useful? React with 👍 / 👎.
What
Adds the pagination half of two ROADMAP items:
The outside-repo / grouping / session-fallback / session-vs-all messaging shipped in #5. This PR fills the remaining gap: the pager and the interactive-vs-piped result limit.
Behavior (git's pager model)
--json)list/findoutput pages throughlessonly when stdout is an interactive TTY; piped/scripted output is plain text (scriptable). Short output that fits one screen prints directly (less -F).-n Nis given (which always wins).--no-pager,NO_PAGER(any non-empty value), or an empty$PAGER/$TASK_PAGER(git's "cat, don't page"). Pager resolves$TASK_PAGER→$PAGER→less→more;$LESSdefaults toFRXunless the user set it.--jsonis never paged (machine-readable stays un-paged, un-decorated).Implementation
tasklib/pager.py: a pureshould_pagedecision and a defensivepage()that pipes through the pager and falls back to a direct write on any failure. The pipe is forced utf-8 (ticket titles can be non-ASCII; a locale-encoding crash would otherwise escape and kill the list). stdin is always closed infinallyso the pager sees EOF.cli._emit_listis the single call site;_format_tickets/_format_groupsnow return strings (were direct prints) so output can be routed; JSON paths print straight to stdout._effective_limitpicks the cap by interactivity; the-ndefault isNoneso an explicit value is distinguishable.Tests
tests/test_pager.py— decision + routing against a real child-process pager ($TASK_PAGER): utf-8 non-ASCII, broken-pipe / non-zero-exit / missing-binary fallbacks,$LESS=FRXseeding (and not overriding a user$LESS), empty / whitespace pager, closed-streamisatty.tests/test_list_pager.py+tests/test_cli_non_repo.py—cmd_list/cmd_findwiring: interactive paging, piped-not-paged-even-with-pager-configured,--no-pager,NO_PAGER,--jsonnever paged, explicit-nflows into the backend, grouped (cross-project) paging on a TTY, no double-emit to stdout.Docs: README pagination section, AGENTS.md rule, install.py SKILL_MD.
Reviewed pre-commit via
review diff --staged(glm). Two real bugs the review caught were fixed: utf-8 pipe encoding (wastext=True→ locale crash) and stdin-close-on-write-failure (FD/EOF).🤖 Generated with Claude Code