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
55 changes: 53 additions & 2 deletions cmd/entire/cli/mdrender/mdrender.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,19 @@ const DefaultTerminalWidth = 80
// of which indicate a malformed StyleConfig (programmer error) rather
// than a runtime condition. Renderer panics are recovered and returned as
// errors so callers can fall back to raw markdown instead of crashing.
func Render(markdown string, width int, darkBackground bool) (rendered string, err error) {
func Render(markdown string, width int, darkBackground bool) (string, error) {
return renderWithStyles(markdown, width, stylesForBackground(darkBackground))
}

// RenderMuted is Render with a low-chroma palette: hierarchy is conveyed by
// bold + indentation rather than colour, and inline-code/link highlighting is
// dropped. Use it for dense, markdown-heavy output (e.g. the multi-agent
// review dump) where the full palette reads as noisy and hard to scan.
func RenderMuted(markdown string, width int, darkBackground bool) (string, error) {
return renderWithStyles(markdown, width, mutedStyles(darkBackground))
}

func renderWithStyles(markdown string, width int, styles ansi.StyleConfig) (rendered string, err error) {
defer func() {
if r := recover(); r != nil {
rendered = ""
Expand All @@ -45,7 +57,7 @@ func Render(markdown string, width int, darkBackground bool) (rendered string, e
}()

renderer, err := glamour.NewTermRenderer(
glamour.WithStyles(stylesForBackground(darkBackground)),
glamour.WithStyles(styles),
glamour.WithWordWrap(width),
glamour.WithPreservedNewLines(),
)
Expand Down Expand Up @@ -73,6 +85,15 @@ func RenderForWriter(w io.Writer, markdown string) (string, error) {
return Render(markdown, terminalWidth(w), termenv.HasDarkBackground())
}

// RenderMutedForWriter is RenderForWriter using the low-chroma palette (see
// RenderMuted). Non-terminal / NO_COLOR writers still get raw markdown.
func RenderMutedForWriter(w io.Writer, markdown string) (string, error) {
if !shouldRender(w) {
return markdown, nil
}
return RenderMuted(markdown, terminalWidth(w), termenv.HasDarkBackground())
}

// shouldRender returns true if w is a terminal writer and NO_COLOR is unset.
func shouldRender(w io.Writer) bool {
if os.Getenv("NO_COLOR") != "" {
Expand Down Expand Up @@ -168,6 +189,36 @@ func stylesForBackground(darkBackground bool) ansi.StyleConfig {
return styles
}

// mutedStyles returns a calmer variant of the CLI palette for dense,
// markdown-heavy output (the multi-agent review dump). It KEEPS the coloured,
// bold headings — they're sparse and give the same scannable structure as
// dispatch — and only neutralises the HIGH-FREQUENCY inline elements that
// multiply with dense findings and read as noise: the highlight block behind
// inline code (file paths), coloured list bullets, and coloured links. Bold
// emphasis (e.g. severity labels) is left intact.
func mutedStyles(darkBackground bool) ansi.StyleConfig {
styles := stylesForBackground(darkBackground)
neutral := "252"
if !darkBackground {
neutral = "234"
}
// Inline code: drop the background highlight and the orange foreground —
// file paths appear in nearly every finding, so the block + accent read as
// a sea of colour. Keep it as plain (slightly emphasised by mono) text.
styles.Code.Color = strPtr(neutral)
styles.Code.BackgroundColor = nil
// List bullets / enumeration markers: neutral, not orange/indigo.
styles.Item.Color = strPtr(neutral)
styles.Enumeration.Color = strPtr(neutral)
// Links: keep the underline as the affordance, drop the colour + bold so a
// finding full of [file](path) links isn't multi-coloured.
styles.Link.Color = nil
styles.Link.Underline = boolPtrV(true)
styles.LinkText.Color = nil
styles.LinkText.Bold = boolPtrV(false)
return styles
}

// chromaForBackground returns the syntax-highlighting palette for code
// blocks. Dark and light backgrounds use distinct text colors but share
// the same accent colors for keywords/functions/literals.
Expand Down
63 changes: 63 additions & 0 deletions cmd/entire/cli/review/banner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package review

import (
"fmt"
"strings"
)

// formatContextBanner returns the transparency block printed below the scope
// banner. It itemises the prior checkpoint/session context `entire review` is
// folding into the agent prompt so the user can see exactly what's being
// reviewed — the value over running the underlying skill manually. The block
// is never omitted; the empty variant reassures the user nothing went wrong,
// there simply is no history.
//
// Example:
//
// Checkpoints in scope (2):
// • a3b2c4d5 feat(review): emit honest live tokens
// • b4c3d5e6 feat(review): flag-driven roles
// In-progress sessions (1):
// • ac3d5c6e Claude Code
//
// When counts are present but the itemised slices aren't populated (defensive),
// it falls back to a one-line count summary.
func formatContextBanner(r ContextResult) string {
if r.Checkpoints == 0 && r.Sessions == 0 {
return "No prior session or checkpoint context for this branch yet."
}
var b strings.Builder
switch {
case len(r.CheckpointItems) > 0:
fmt.Fprintf(&b, "Checkpoints in scope (%d):\n", len(r.CheckpointItems))
for _, c := range r.CheckpointItems {
summary := c.Summary
if summary == "" {
summary = "(no summary)"
}
fmt.Fprintf(&b, " • %s %s\n", c.ID, summary)
}
case r.Checkpoints > 0:
fmt.Fprintf(&b, "%s in scope.\n", pluralizeContextNoun(r.Checkpoints, "checkpoint", "checkpoints"))
}
switch {
case len(r.SessionItems) > 0:
fmt.Fprintf(&b, "In-progress sessions (%d):\n", len(r.SessionItems))
for _, s := range r.SessionItems {
fmt.Fprintf(&b, " • %s %s\n", s.ID, s.Agent)
}
case r.Sessions > 0:
fmt.Fprintf(&b, "%s in progress.\n", pluralizeContextNoun(r.Sessions, "session", "sessions"))
}
return strings.TrimRight(b.String(), "\n")
}

// pluralizeContextNoun returns "<n> <singular>" when n == 1 and
// "<n> <plural>" otherwise. Kept private to banner.go; the review package
// has no other plural cases that would justify a shared utility.
func pluralizeContextNoun(n int, singular, plural string) string {
if n == 1 {
return fmt.Sprintf("%d %s", n, singular)
}
return fmt.Sprintf("%d %s", n, plural)
}
92 changes: 92 additions & 0 deletions cmd/entire/cli/review/banner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package review

import "testing"

// TestFormatContextBanner pins the itemised scope banner: an empty state, the
// itemised checkpoints+sessions layout, and the count-only fallback used when
// items aren't populated.
func TestFormatContextBanner(t *testing.T) {
t.Parallel()

tests := []struct {
name string
in ContextResult
want string
}{
{
name: "neither",
in: ContextResult{},
want: "No prior session or checkpoint context for this branch yet.",
},
{
name: "itemised checkpoints and sessions",
in: ContextResult{
Checkpoints: 2, Sessions: 1,
CheckpointItems: []CheckpointScopeItem{
{ID: "a3b2c4d5", Summary: "feat(review): emit honest live tokens"},
{ID: "b4c3d5e6", Summary: "feat(review): flag-driven roles"},
},
SessionItems: []SessionScopeItem{
{ID: "ac3d5c6e", Agent: "Claude Code"},
},
},
want: "Checkpoints in scope (2):\n" +
" • a3b2c4d5 feat(review): emit honest live tokens\n" +
" • b4c3d5e6 feat(review): flag-driven roles\n" +
"In-progress sessions (1):\n" +
" • ac3d5c6e Claude Code",
},
{
name: "sessions listed by short id and agent",
in: ContextResult{
Sessions: 2,
SessionItems: []SessionScopeItem{
{ID: "ac3d5c6e", Agent: "Claude Code"},
{ID: "3d4c9f88", Agent: "Codex"},
},
},
want: "In-progress sessions (2):\n" +
" • ac3d5c6e Claude Code\n" +
" • 3d4c9f88 Codex",
},
{
name: "count-only fallback when items absent",
in: ContextResult{Checkpoints: 3, Sessions: 1},
want: "3 checkpoints in scope.\n1 session in progress.",
},
{
name: "empty summary renders placeholder",
in: ContextResult{
Checkpoints: 1,
CheckpointItems: []CheckpointScopeItem{{ID: "a3b2c4d5"}},
},
want: "Checkpoints in scope (1):\n • a3b2c4d5 (no summary)",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := formatContextBanner(tc.in); got != tc.want {
t.Errorf("formatContextBanner(%+v) =\n%q\nwant\n%q", tc.in, got, tc.want)
}
})
}
}

func TestPluralizeContextNoun(t *testing.T) {
t.Parallel()

tests := []struct {
n int
want string
}{
{n: 1, want: "1 checkpoint"},
{n: 2, want: "2 checkpoints"},
{n: 0, want: "0 checkpoints"},
}
for _, tc := range tests {
if got := pluralizeContextNoun(tc.n, "checkpoint", "checkpoints"); got != tc.want {
t.Errorf("pluralizeContextNoun(%d) = %q, want %q", tc.n, got, tc.want)
}
}
}
Loading
Loading