Skip to content

fix(pager): render static diffs in captured pagers#271

Merged
benvinegar merged 6 commits intomainfrom
fix/static-pager-output
May 10, 2026
Merged

fix(pager): render static diffs in captured pagers#271
benvinegar merged 6 commits intomainfrom
fix/static-pager-output

Conversation

@benvinegar
Copy link
Copy Markdown
Member

@benvinegar benvinegar commented May 10, 2026

Summary

  • Add a static ANSI diff renderer for hunk pager when it is launched by captured pager hosts such as LazyGit.
  • Keep normal interactive pager behavior when Hunk can attach to a terminal, and preserve raw passthrough for non-TTY stdout/pipelines or plain TERM=dumb terminals.
  • Reuse Hunk/Pierre parsing, highlighting, row planning, and shared row styling helpers so static output stays visually aligned with the TUI.
  • Include semantic file metadata in static headers without exposing raw patch transport headers.
  • Apply Hunk config to static output for supported view options such as theme, line numbers, and hunk headers.
  • Emit a diagnostic before falling back to raw diff if static rendering fails.

Verification

  • bun test src/ui/staticDiffPager.test.ts src/core/startup.test.ts test/cli/entrypoint.test.ts
  • bun run typecheck
  • Manual LazyGit check with git.pagers[].pager = "bun /home/bentlegen/Projects/hunk-b/src/main.tsx pager"

This PR description was generated by Pi using OpenAI GPT-5

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 10, 2026

Greptile Summary

This PR adds a static ANSI diff renderer for hunk pager when running inside captured pager hosts (e.g., LazyGit) that cannot host the interactive TUI. Three routing cases are introduced in prepareStartupPlan: raw passthrough when stdout is not a TTY, static colored output when TERM=dumb (captured PTY), and static colored output when no controlling terminal can be opened.

  • src/ui/staticDiffPager.ts is a new renderer that reuses the existing loadAppBootstrap + theme + buildStackRows pipeline to produce truecolor ANSI output without alternate-screen sequences.
  • src/core/startup.ts is refactored to initialise controllingTerminal earlier and to clean it up on bootstrap failure, fixing a pre-existing resource leak.
  • All three new paths (passthrough, static-diff-pager) are exercised in unit and integration tests.

Confidence Score: 4/5

Safe to merge. The new rendering path cannot corrupt or lose data — it falls back to raw text on any failure — and the TTY detection logic is guarded by tests covering all three new branches.

The core change is well-tested and the fallback design means a rendering bug produces at worst uncolored diff output. The two findings are non-blocking: the silent catch makes render failures invisible to developers, and the TERM=dumb heuristic could emit ANSI codes on genuine dumb terminals, though the previous behavior (launching the full TUI) was already broken in that scenario.

src/ui/staticDiffPager.ts (error observability) and src/core/startup.ts (TERM=dumb breadth).

Important Files Changed

Filename Overview
src/ui/staticDiffPager.ts New static ANSI renderer that wraps loadAppBootstrap + theme resolution. The bare catch in renderStaticDiffPager silently swallows all errors including unexpected ones. Rendering logic for per-row colors, line numbers, and file metadata looks correct.
src/core/startup.ts Adds passthrough and static-diff-pager plan types with TTY/TERM checks in the pager branch. Controlling terminal lifetime is correctly guarded with try/catch. The TERM=dumb heuristic assumes LazyGit context but could affect genuine dumb terminals.
src/main.tsx Clean handling of the two new startup plan types (passthrough and static-diff-pager) before the existing app path. No issues found.
src/core/startup.test.ts Three new tests covering passthrough (non-TTY), static-diff-pager (TERM=dumb), and static-diff-pager (no controlling terminal). Well-structured, each asserts the correct plan shape and confirms loadAppBootstrap is not called.
src/ui/staticDiffPager.test.ts Tests rendering output, semantic file headers, and the parse-failure fallback. Coverage is adequate for the happy paths.
test/cli/entrypoint.test.ts Integration test validates the passthrough behavior by spawning a real process with piped stdout — confirms exit 0, no alternate-screen codes, and exact stdin pass-through.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[hunk pager called] --> B{stdin looks like a patch?}
    B -- No --> C[plain-text-pager\nTUI plain text viewer]
    B -- Yes --> D{stdoutIsTTY?}
    D -- No --> E[passthrough\nwrite stdin to stdout]
    D -- Yes --> F{TERM=dumb?}
    F -- Yes --> G[static-diff-pager\nANSI colored diff]
    F -- No --> H{controllingTerminal\navailable?}
    H -- No --> G
    H -- Yes --> I[app\nlaunch interactive TUI]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/ui/staticDiffPager.ts:161-182
**Silent catch swallows all render errors**

The bare `catch` at the bottom discards every possible error — including unexpected programming bugs, type mismatches, or broken theme lookups — and returns the original `text` with no diagnostic output. A user whose render silently fails sees the raw diff instead of a colored one and has no indication that something went wrong; a developer can't tell whether the fallback was triggered by an expected parse failure or an unintended regression.

Consider emitting at least a one-line warning to `process.stderr` (or re-exporting the caught value for test inspection) so the fallback path is observable without changing the user-visible behavior.

### Issue 2 of 2
src/core/startup.ts:117-125
**`TERM=dumb` heuristic can misfire on genuine dumb terminals**

The comment says this targets captured pager hosts like LazyGit, but `TERM=dumb` is a standard signal that the terminal does not support ANSI escape codes at all (e.g., a serial console, a minimal SSH session, or certain CI runners). When hunk is configured as a git pager on such a system with `stdoutIsTTY=true`, this branch emits ANSI truecolor codes to a terminal that explicitly declared it cannot render them, producing visible escape sequence garbage. Before this PR the same scenario would have attempted a full TUI, so this is still an improvement — but a narrower condition (e.g., also checking for a `LAZYGIT=1` env var or a well-known LazyGit variable) would be safer.

Reviews (1): Last reviewed commit: "fix(pager): render static diffs in captu..." | Re-trigger Greptile

Comment thread src/ui/staticDiffPager.ts
Comment on lines +161 to +182
/** Render diff-like pager stdin as colored static output, falling back to the original patch on failure. */
export async function renderStaticDiffPager(text: string, options: CommonOptions = {}) {
try {
const bootstrap = await loadAppBootstrap({
kind: "patch",
file: "-",
text,
options: {
...options,
pager: true,
},
});
const theme = resolveTheme(options.theme, null);
const rendered = await Promise.all(
bootstrap.changeset.files.map((file) => renderStaticFile(file, theme)),
);

return rendered.length > 0 ? `${rendered.join("\n\n")}\n` : text;
} catch {
return text;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent catch swallows all render errors

The bare catch at the bottom discards every possible error — including unexpected programming bugs, type mismatches, or broken theme lookups — and returns the original text with no diagnostic output. A user whose render silently fails sees the raw diff instead of a colored one and has no indication that something went wrong; a developer can't tell whether the fallback was triggered by an expected parse failure or an unintended regression.

Consider emitting at least a one-line warning to process.stderr (or re-exporting the caught value for test inspection) so the fallback path is observable without changing the user-visible behavior.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/staticDiffPager.ts
Line: 161-182

Comment:
**Silent catch swallows all render errors**

The bare `catch` at the bottom discards every possible error — including unexpected programming bugs, type mismatches, or broken theme lookups — and returns the original `text` with no diagnostic output. A user whose render silently fails sees the raw diff instead of a colored one and has no indication that something went wrong; a developer can't tell whether the fallback was triggered by an expected parse failure or an unintended regression.

Consider emitting at least a one-line warning to `process.stderr` (or re-exporting the caught value for test inspection) so the fallback path is observable without changing the user-visible behavior.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an observable fallback diagnostic. renderStaticDiffPager now writes a one-line warning before returning the raw diff when static rendering fails or renders no files, and the fallback test asserts that diagnostic.

This comment was generated by Pi using OpenAI GPT-5

Comment thread src/core/startup.ts
Comment on lines +117 to +125
// Captured pager hosts like LazyGit can provide a PTY while advertising TERM=dumb.
// In that mode, emit static colored diff output instead of launching the TUI.
if (env.TERM === "dumb") {
return {
kind: "static-diff-pager",
text: stdinText,
options: parsedCliInput.options,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 TERM=dumb heuristic can misfire on genuine dumb terminals

The comment says this targets captured pager hosts like LazyGit, but TERM=dumb is a standard signal that the terminal does not support ANSI escape codes at all (e.g., a serial console, a minimal SSH session, or certain CI runners). When hunk is configured as a git pager on such a system with stdoutIsTTY=true, this branch emits ANSI truecolor codes to a terminal that explicitly declared it cannot render them, producing visible escape sequence garbage. Before this PR the same scenario would have attempted a full TUI, so this is still an improvement — but a narrower condition (e.g., also checking for a LAZYGIT=1 env var or a well-known LazyGit variable) would be safer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/startup.ts
Line: 117-125

Comment:
**`TERM=dumb` heuristic can misfire on genuine dumb terminals**

The comment says this targets captured pager hosts like LazyGit, but `TERM=dumb` is a standard signal that the terminal does not support ANSI escape codes at all (e.g., a serial console, a minimal SSH session, or certain CI runners). When hunk is configured as a git pager on such a system with `stdoutIsTTY=true`, this branch emits ANSI truecolor codes to a terminal that explicitly declared it cannot render them, producing visible escape sequence garbage. Before this PR the same scenario would have attempted a full TUI, so this is still an improvement — but a narrower condition (e.g., also checking for a `LAZYGIT=1` env var or a well-known LazyGit variable) would be safer.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrowed this so a plain TERM=dumb terminal now gets raw passthrough rather than ANSI static output. The static path is limited to likely captured pager hosts (for example LazyGit-style pager env) or the separate no-controlling-terminal fallback, with a regression test for the plain dumb terminal case.

This comment was generated by Pi using OpenAI GPT-5

@benvinegar benvinegar merged commit e974167 into main May 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant