feat: TTY-Aware Output Rendering#20
Conversation
Four display-only DX features, all TTY-gated to preserve the pipe-friendliness contract (Constitution VI) — piped/redirected output stays full and canonical: - A: rune-safe text truncation with --full override and an optional 'idea ls [id...]' positional filter - B: 'idea prune' dry-run stderr count header - D: TTY + NO_COLOR-gated color (dim prefix, green [x]) - E: interactive [y/N] confirm for 'idea prune' (--force skips) Adds golang.org/x/term (pinned v0.27.0) as the first non-stdlib/cobra direct dependency for isatty + terminal width, justified under Dependency Discipline; go directive kept at 1.22. Backlog format, parser, and --json schema are unchanged.
There was a problem hiding this comment.
Pull request overview
This PR adds TTY-aware rendering behavior to the idea CLI so ls/prune output remains scannable on terminals while preserving canonical, pipe-friendly output when stdout is redirected/piped.
Changes:
- Introduces an
internal/ideaterminal seam (IsTTY,TermWidth,UseColor,DisplayListLine) supporting truncation, multiline clipping, and optional ANSI styling. - Updates
idea list/lsto support--fulland optional[id...]filtering, and routes list output through a shared TTY-aware renderer. - Updates
idea pruneto add a stderr count header in dry-run mode and a TTY-gated interactive[y/N]confirmation flow; adds/extends unit + integration tests.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/internal/idea/term.go | New terminal seam: TTY detection, width resolution, NO_COLOR-gated ANSI styling, and display-line truncation logic. |
| src/internal/idea/term_test.go | Unit tests for terminal width fallback, TTY detection, color helpers, and truncation behavior. |
| src/cmd/idea/output.go | Shared printIdeaLines helper to centralize TTY-aware list/prune rendering policy. |
| src/cmd/idea/list.go | Adds --full, optional [id...] filter, and uses shared output helper for TTY-aware rendering. |
| src/cmd/idea/prune.go | Adds stderr count header, --full, and TTY-only interactive confirmation prior to deletion. |
| src/cmd/idea/main_test.go | Expands CLI integration coverage for list ID filtering, piped canonical output, prune header/decision matrix, and confirm logic. |
| src/go.mod | Adds direct dependency on golang.org/x/term v0.27.0. |
| src/go.sum | Adds golang.org/x/term and indirect golang.org/x/sys checksums. |
| docs/memory/cli/structure.md | Documents new terminal seam and shared renderer path; updates dependency notes. |
| docs/memory/cli/list.md | New doc describing list TTY behavior, --full, [id...] filter, and pipe contract. |
| docs/memory/cli/prune.md | Updates prune docs with stderr header + TTY confirm decision matrix and rendering behavior. |
| docs/memory/cli/index.md | Adds list doc entry and updates descriptions (but has inconsistent “Last Updated” values). |
| fab/changes/260613-kfcl-tty-aware-output-rendering/intake.md | Change intake/spec for the feature bundle. |
| fab/changes/260613-kfcl-tty-aware-output-rendering/plan.md | Implementation plan and acceptance checklist. |
| fab/changes/260613-kfcl-tty-aware-output-rendering/.status.yaml | Pipeline/status metadata for the change. |
| fab/changes/260613-kfcl-tty-aware-output-rendering/.history.jsonl | Change history log. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if tt.setEnv { | ||
| t.Setenv("COLUMNS", tt.columns) | ||
| } else { | ||
| os.Unsetenv("COLUMNS") | ||
| } |
There was a problem hiding this comment.
Fixed — replaced os.Unsetenv("COLUMNS") with a t.Cleanup-based unsetEnvForTest helper that captures the prior value and restores it after the test, so the unset state no longer leaks. (bc17df0)
| // Not a TTY: always false, even with NO_COLOR unset. | ||
| os.Unsetenv("NO_COLOR") | ||
| if UseColor(f) { | ||
| t.Error("UseColor(non-tty) = true, want false (TTY gate)") | ||
| } |
There was a problem hiding this comment.
Fixed — NO_COLOR is now unset via the same t.Cleanup-based unsetEnvForTest helper, capturing and restoring the prior value so it cannot leak into other tests. (bc17df0)
| for _, id := range wantIDs { | ||
| if !found[id] { | ||
| fmt.Fprintf(cmd.ErrOrStderr(), "warning: no idea with ID %q\n", id) | ||
| } | ||
| } |
There was a problem hiding this comment.
Fixed — filterByIDs now tracks warned IDs in a set so each missing ID is warned at most once, even when passed multiple times (e.g. idea ls zzzz zzzz). (bc17df0)
| // Confirmed: perform the deletion via the same force path. | ||
| _, backfilled, err = idea.Prune(path, true) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| printBackfillNotice(cmd, backfilled) | ||
| fmt.Printf("Pruned %d done idea(s).\n", len(pruned)) | ||
| return nil |
There was a problem hiding this comment.
Fixed — the confirmed idea.Prune(path, true) call now assigns its returned pruned slice, and the final count is taken from that slice rather than the earlier dry run, so the reported total is accurate even if the file changed in between. (bc17df0)
| | [edit](edit.md) | `idea edit` two-form contract: inline replacement (two-arg form) vs. $EDITOR round-trip (`edit <query>`) — editor resolution chain, temp-file mechanics, edge/exit semantics, and the fake-editor test seam | 2026-06-12 | | ||
| | [prune](prune.md) | Bulk-remove subcommand (`idea prune`): dry-run-by-default/`--force` contract, stdout/stderr channel split, exit codes, the deliberate non-archival design, and the `removeIdeaAt` seam shared with `Rm` | 2026-06-12 | | ||
| | [structure](structure.md) | Source tree layout (cmd/idea + internal/idea), root command factory, command aliases vs. the bare-text shorthand, backlog line lifecycle (lenient read / canonical write incl. the escaped-text convention for multiline ideas and the explicit `idea fmt` canonicalizer with bare-checkbox adoption), help-dump contract, and version stamping | 2026-06-12 | | ||
| | [list](list.md) | `idea list`/`ls` rendering contract: TTY-aware rune-safe text truncation, the `--full` flag, the optional `[id...]` positional filter, ANSI color (NO_COLOR-gated), and the pipe contract that keeps piped output canonical | — | | ||
| | [prune](prune.md) | Bulk-remove subcommand (`idea prune`): the TTY × `--force` decision matrix (pipe dry-run vs. interactive `[y/N]` confirm), the leading stderr count header, TTY-aware truncation/color/`--full` listing, stdout/stderr channel split, exit codes, the deliberate non-archival design, and the `removeIdeaAt` seam shared with `Rm` | 2026-06-12 | | ||
| | [structure](structure.md) | Source tree layout (cmd/idea + internal/idea), root command factory, command aliases vs. the bare-text shorthand, backlog line lifecycle (lenient read / canonical write incl. the escaped-text convention for multiline ideas and the explicit `idea fmt` canonicalizer with bare-checkbox adoption), the TTY/width/color/truncation seam (internal/idea/term.go) + shared printIdeaLines render path, the golang.org/x/term direct dependency, help-dump contract, and version stamping | 2026-06-12 | |
There was a problem hiding this comment.
Fixed — regenerated the index via fab memory-index; list, prune, and structure now correctly show 2026-06-13 (their commit date in this PR), consistent with the generated-from-git-log contract. (bc17df0)
Meta
Pipeline: intake ✓ → apply ✓ → review ✓ → hydrate ✓ → ship → review-pr
Impact: +814/−26 code (excluding
fab/,docs/) · +1349/−49 totalSummary
idea lsandidea prune(dry-run) print each idea's full untruncated text, and ideas in this project are frequently paragraph-length. On a TTY these soft-wrap into many visual rows each, so theprune--forceconfirm hint scrolls off-screen andlsbecomes unscannable. This change adds four TTY-aware DX features for theideaCLI to make list and prune output scannable on a terminal — all gated so piped/redirected output stays full and canonical, preserving the pipe-friendliness contract (Constitution VI). Display-only: the backlog format, parser, and--jsonschema are unchanged.Changes
--fullflag, andls [id...]filter: On a TTY (unless--full), each idea's text portion is truncated to fit the terminal width with a…ellipsis; the[id] date:prefix is never truncated. Truncation is rune-safe and clips multiline ideas at the first newline.idea lsgains a--fulloverride and an optional variadic[id...]positional filter (unknown IDs warn on stderr and the rest still list). Non-TTY output is always full/canonical.idea prunedry-run count header: A one-lineN done idea(s) would be pruned …summary is printed to stderr before the list, so the action is the first thing a human sees while stdout still carries exactly the removable lines (2>/dev/nullsuppresses the header consistently).NO_COLOR-gated): When stdout is a TTY andNO_COLORis unset, the[id] date:prefix is dimmed and the done[x]checkbox is green; color is applied after truncation so width math counts visible runes. Plain output otherwise.[y/N]confirm foridea prune: On a TTY without--force, prune lists the prunable ideas then promptsPrune N done idea(s)? [y/N]on stderr as the last line at the cursor — inherently un-buryable.--forcestill skips the prompt (script behavior unchanged); non-TTY never prompts (would hang a pipe) and falls back to today's dry-run.Dependency: adds
golang.org/x/term(pinned v0.27.0) as the first non-stdlib/cobra direct dependency, used for isatty + terminal-width detection in a newinternal/idea/term.goseam (stdlib has no terminal-width primitive). Justified under the constitution's Dependency Discipline. Thegodirective is kept at 1.22 — v0.27.0 is the latestx/termthat does not bump the minimum Go version.See change folder:
fab/changes/260613-kfcl-tty-aware-output-rendering/.🤖 Generated with Claude Code