Skip to content

Commit ed9ff97

Browse files
peyton-altclaude
andcommitted
Render review output through shared markdown palette (CU13 of PR-B)
Agents emit markdown — claude-code, codex, and gemini-cli all return narrative with headings, lists, code blocks, and inline formatting. Until now DumpSink printed it as raw text, so the user saw bare ─────── name review ─────── separators followed by unstyled markdown like `# foo` and `- bar`. Same for the cross-agent synthesis verdict. This commit: - Extracts the glamour palette from dispatch_tui.go into a shared cmd/entire/cli/mdrender/ package. Render(markdown, width, dark) is the pure primitive; RenderForWriter(w, markdown) is TTY-aware and returns raw markdown for non-TTY writers (so redirected output stays grep-friendly and pipeline-safe). - Updates DumpSink to compose each agent's section as markdown: H1 heading for agent name; cancelled runs render as "_cancelled_"; failed runs use **Failed: `err`** plus blockquoted RunError details; succeeded runs render the AssistantText narrative directly. The counts line at the end stays plain (it's a status summary, not narrative content — keeping it grep-friendly is more useful than styling it). - Updates SynthesisSink to render the cross-agent verdict through the same palette. - Updates dispatch_tui.go's defaultRenderTerminalMarkdown to delegate to mdrender.Render — preserving its existing always-render behavior (dispatch emits ANSI even when redirected, by long-standing intent). The TUI's drill-in screen and dashboard rows are unchanged; lipgloss styling there already does its job and doesn't need glamour. Tests: TTY/non-TTY paths in mdrender_test.go, NO_COLOR honored, dark vs light palette both produce styled output. DumpSink tests rewritten to assert markdown body (since bytes.Buffer is non-TTY). All existing tests continue to pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f53a0ee commit ed9ff97

6 files changed

Lines changed: 397 additions & 227 deletions

File tree

cmd/entire/cli/dispatch_tui.go

Lines changed: 8 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ import (
1010
"github.com/charmbracelet/bubbles/key"
1111
"github.com/charmbracelet/bubbles/spinner"
1212
tea "github.com/charmbracelet/bubbletea"
13-
"github.com/charmbracelet/glamour"
14-
"github.com/charmbracelet/glamour/ansi"
15-
glamourstyles "github.com/charmbracelet/glamour/styles"
1613
"github.com/charmbracelet/lipgloss"
17-
dispatchpkg "github.com/entireio/cli/cmd/entire/cli/dispatch"
1814
"github.com/muesli/termenv"
15+
16+
dispatchpkg "github.com/entireio/cli/cmd/entire/cli/dispatch"
17+
"github.com/entireio/cli/cmd/entire/cli/mdrender"
1918
)
2019

2120
type dispatchRenderResult struct {
@@ -92,21 +91,12 @@ func defaultRunInteractiveDispatch(ctx context.Context, outW io.Writer, opts dis
9291
return finished.result.markdown, nil
9392
}
9493

94+
// defaultRenderTerminalMarkdown renders dispatch's LLM markdown output via
95+
// the shared mdrender palette. Always renders (no TTY check) — dispatch's
96+
// existing behavior is to emit ANSI codes even when redirected so that
97+
// `entire dispatch | less -R` still shows colors.
9598
func defaultRenderTerminalMarkdown(w io.Writer, markdown string) (string, error) {
96-
renderer, err := glamour.NewTermRenderer(
97-
glamour.WithStyles(dispatchMarkdownStyles()),
98-
glamour.WithWordWrap(getTerminalWidth(w)),
99-
glamour.WithPreservedNewLines(),
100-
)
101-
if err != nil {
102-
return "", fmt.Errorf("initialize markdown renderer: %w", err)
103-
}
104-
105-
rendered, err := renderer.Render(markdown)
106-
if err != nil {
107-
return "", fmt.Errorf("render markdown: %w", err)
108-
}
109-
return rendered, nil
99+
return mdrender.Render(markdown, getTerminalWidth(w), termenv.HasDarkBackground()) //nolint:wrapcheck // mdrender already wraps glamour's errors with package context
110100
}
111101

112102
func newDispatchStatusModel(
@@ -158,185 +148,6 @@ func newDispatchStatusStyles(ss statusStyles) dispatchStatusStyles {
158148
return styles
159149
}
160150

161-
func dispatchMarkdownStyles() ansi.StyleConfig {
162-
return dispatchMarkdownStylesForBackground(termenv.HasDarkBackground())
163-
}
164-
165-
func dispatchMarkdownStylesForBackground(darkBackground bool) ansi.StyleConfig {
166-
var styles ansi.StyleConfig
167-
if darkBackground {
168-
styles = glamourstyles.DarkStyleConfig
169-
} else {
170-
styles = glamourstyles.LightStyleConfig
171-
}
172-
173-
if darkBackground {
174-
styles.Document.Color = stringPtr("252")
175-
styles.Heading.Color = stringPtr("252")
176-
styles.Code.BackgroundColor = stringPtr("236")
177-
styles.CodeBlock.Color = stringPtr("252")
178-
} else {
179-
styles.Document.Color = stringPtr("234")
180-
styles.Heading.Color = stringPtr("234")
181-
styles.Code.BackgroundColor = stringPtr("254")
182-
styles.CodeBlock.Color = stringPtr("242")
183-
}
184-
styles.Heading.Bold = boolPtr(true)
185-
186-
styles.H1.Prefix = "# "
187-
styles.H1.Suffix = ""
188-
styles.H1.Color = stringPtr("#fb923c")
189-
styles.H1.BackgroundColor = nil
190-
styles.H1.Bold = boolPtr(true)
191-
192-
styles.H2.Color = stringPtr("#22d3ee")
193-
styles.H2.Bold = boolPtr(true)
194-
styles.H3.Color = stringPtr("#818cf8")
195-
styles.H3.Bold = boolPtr(true)
196-
styles.H4.Color = stringPtr("252")
197-
styles.H4.Bold = boolPtr(true)
198-
styles.H5.Color = stringPtr("245")
199-
styles.H5.Bold = boolPtr(true)
200-
styles.H6.Color = stringPtr("245")
201-
styles.H6.Bold = boolPtr(false)
202-
203-
styles.HorizontalRule.Color = stringPtr("240")
204-
styles.Item.Color = stringPtr("#fb923c")
205-
styles.Enumeration.Color = stringPtr("#818cf8")
206-
styles.BlockQuote.Color = stringPtr("245")
207-
208-
styles.Link.Color = stringPtr("#22d3ee")
209-
styles.Link.Underline = boolPtr(true)
210-
styles.LinkText.Color = stringPtr("#818cf8")
211-
styles.LinkText.Bold = boolPtr(true)
212-
213-
styles.Code.Color = stringPtr("#fb923c")
214-
if darkBackground {
215-
styles.CodeBlock.Chroma = &ansi.Chroma{
216-
Text: ansi.StylePrimitive{
217-
Color: stringPtr("252"),
218-
},
219-
Error: ansi.StylePrimitive{
220-
Color: stringPtr("252"),
221-
},
222-
Comment: ansi.StylePrimitive{
223-
Color: stringPtr("245"),
224-
Italic: boolPtr(true),
225-
},
226-
Keyword: ansi.StylePrimitive{
227-
Color: stringPtr("#818cf8"),
228-
Bold: boolPtr(true),
229-
},
230-
KeywordReserved: ansi.StylePrimitive{
231-
Color: stringPtr("#818cf8"),
232-
Bold: boolPtr(true),
233-
},
234-
Name: ansi.StylePrimitive{
235-
Color: stringPtr("252"),
236-
},
237-
NameFunction: ansi.StylePrimitive{
238-
Color: stringPtr("#22d3ee"),
239-
},
240-
NameBuiltin: ansi.StylePrimitive{
241-
Color: stringPtr("#818cf8"),
242-
},
243-
Literal: ansi.StylePrimitive{
244-
Color: stringPtr("#fbbf24"),
245-
},
246-
LiteralString: ansi.StylePrimitive{
247-
Color: stringPtr("#fbbf24"),
248-
},
249-
LiteralNumber: ansi.StylePrimitive{
250-
Color: stringPtr("#fbbf24"),
251-
},
252-
Operator: ansi.StylePrimitive{
253-
Color: stringPtr("244"),
254-
},
255-
Punctuation: ansi.StylePrimitive{
256-
Color: stringPtr("244"),
257-
},
258-
GenericDeleted: ansi.StylePrimitive{
259-
Color: stringPtr("1"),
260-
},
261-
GenericInserted: ansi.StylePrimitive{
262-
Color: stringPtr("2"),
263-
},
264-
Background: ansi.StylePrimitive{
265-
BackgroundColor: stringPtr("236"),
266-
},
267-
}
268-
} else {
269-
styles.CodeBlock.Chroma = &ansi.Chroma{
270-
Text: ansi.StylePrimitive{
271-
Color: stringPtr("#2A2A2A"),
272-
},
273-
Error: ansi.StylePrimitive{
274-
Color: stringPtr("#2A2A2A"),
275-
},
276-
Comment: ansi.StylePrimitive{
277-
Color: stringPtr("#8D8D8D"),
278-
Italic: boolPtr(true),
279-
},
280-
Keyword: ansi.StylePrimitive{
281-
Color: stringPtr("#818cf8"),
282-
Bold: boolPtr(true),
283-
},
284-
KeywordReserved: ansi.StylePrimitive{
285-
Color: stringPtr("#818cf8"),
286-
Bold: boolPtr(true),
287-
},
288-
Name: ansi.StylePrimitive{
289-
Color: stringPtr("#2A2A2A"),
290-
},
291-
NameFunction: ansi.StylePrimitive{
292-
Color: stringPtr("#22d3ee"),
293-
},
294-
NameBuiltin: ansi.StylePrimitive{
295-
Color: stringPtr("#818cf8"),
296-
},
297-
Literal: ansi.StylePrimitive{
298-
Color: stringPtr("#fbbf24"),
299-
},
300-
LiteralString: ansi.StylePrimitive{
301-
Color: stringPtr("#fbbf24"),
302-
},
303-
LiteralNumber: ansi.StylePrimitive{
304-
Color: stringPtr("#fbbf24"),
305-
},
306-
Operator: ansi.StylePrimitive{
307-
Color: stringPtr("#7A7A7A"),
308-
},
309-
Punctuation: ansi.StylePrimitive{
310-
Color: stringPtr("#7A7A7A"),
311-
},
312-
GenericDeleted: ansi.StylePrimitive{
313-
Color: stringPtr("1"),
314-
},
315-
GenericInserted: ansi.StylePrimitive{
316-
Color: stringPtr("2"),
317-
},
318-
Background: ansi.StylePrimitive{
319-
BackgroundColor: stringPtr("254"),
320-
},
321-
}
322-
}
323-
324-
styles.Table.Color = stringPtr("245")
325-
styles.Table.CenterSeparator = stringPtr(" ")
326-
styles.Table.ColumnSeparator = stringPtr(" ")
327-
styles.Table.RowSeparator = stringPtr("-")
328-
329-
return styles
330-
}
331-
332-
func boolPtr(v bool) *bool {
333-
return &v
334-
}
335-
336-
func stringPtr(v string) *string {
337-
return &v
338-
}
339-
340151
func dispatchStatusDetails(opts dispatchpkg.Options) []string {
341152
scope := "Scope: current repo"
342153
if len(opts.RepoPaths) > 0 {

0 commit comments

Comments
 (0)