Coverage for lintro / ai / display / summary.py: 93%
130 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Summary rendering for terminal, GitHub Actions, and Markdown."""
3from __future__ import annotations
5import io
7from rich.console import Console, Group, RenderableType
8from rich.markup import escape
9from rich.panel import Panel
11from lintro.ai.cost import format_cost
12from lintro.ai.display.shared import (
13 LEADING_NUMBER_RE,
14 cost_str,
15 is_github_actions,
16 print_section_header,
17)
18from lintro.ai.models import AISummary
19from lintro.utils.console.constants import BORDER_LENGTH
22def render_summary_terminal(
23 summary: AISummary,
24 *,
25 show_cost: bool = True,
26) -> str:
27 """Render AI summary for terminal output.
29 Uses Rich Panels with a structured layout for overview,
30 key patterns, priority actions, and effort estimate.
32 Args:
33 summary: AI summary to render.
34 show_cost: Whether to show cost estimates.
36 Returns:
37 Formatted string for terminal display.
38 """
39 if not summary.overview:
40 return ""
42 buf = io.StringIO()
43 console = Console(
44 file=buf,
45 force_terminal=True,
46 highlight=False,
47 width=BORDER_LENGTH,
48 )
50 cost_info = (
51 cost_str(summary.input_tokens, summary.output_tokens, summary.cost_estimate)
52 if show_cost
53 else ""
54 )
56 print_section_header(
57 console,
58 "\U0001f9e0",
59 "AI SUMMARY",
60 "actionable insights",
61 cost_info=cost_info,
62 )
64 parts: list[RenderableType] = []
66 # Overview
67 parts.append(f"[cyan]{escape(summary.overview)}[/cyan]")
69 # Key patterns
70 if summary.key_patterns:
71 parts.append("")
72 parts.append("[bold yellow]Key Patterns:[/bold yellow]")
73 for pattern in summary.key_patterns:
74 parts.append(f" [yellow]\u2022[/yellow] {escape(pattern)}")
76 # Priority actions
77 if summary.priority_actions:
78 parts.append("")
79 parts.append("[bold green]Priority Actions:[/bold green]")
80 for i, action in enumerate(summary.priority_actions, 1):
81 clean = LEADING_NUMBER_RE.sub("", action)
82 parts.append(f" [green]{i}.[/green] {escape(clean)}")
84 # Triage suggestions
85 if summary.triage_suggestions:
86 parts.append("")
87 parts.append("[bold magenta]Triage \u2014 Consider Suppressing:[/bold magenta]")
88 for suggestion in summary.triage_suggestions:
89 clean = LEADING_NUMBER_RE.sub("", suggestion)
90 parts.append(f" [magenta]~[/magenta] {escape(clean)}")
92 # Effort estimate
93 if summary.estimated_effort:
94 parts.append("")
95 parts.append(
96 f"[dim]Estimated effort: {escape(summary.estimated_effort)}[/dim]",
97 )
99 content: RenderableType = Group(*parts) if len(parts) > 1 else parts[0]
100 console.print(
101 Panel(
102 content,
103 border_style="cyan",
104 padding=(0, 1),
105 ),
106 )
108 return buf.getvalue()
111def render_summary_github(
112 summary: AISummary,
113 *,
114 show_cost: bool = True,
115) -> str:
116 """Render AI summary for GitHub Actions logs.
118 Args:
119 summary: AI summary to render.
120 show_cost: Whether to show cost estimates.
122 Returns:
123 Formatted string with GitHub Actions group markers.
124 """
125 if not summary.overview:
126 return ""
128 lines: list[str] = []
129 lines.append("::group::AI Summary \u2014 actionable insights")
130 lines.append("")
131 lines.append(summary.overview)
133 if summary.key_patterns:
134 lines.append("")
135 lines.append("Key Patterns:")
136 for pattern in summary.key_patterns:
137 lines.append(f" \u2022 {pattern}")
139 if summary.priority_actions:
140 lines.append("")
141 lines.append("Priority Actions:")
142 for i, action in enumerate(summary.priority_actions, 1):
143 clean = LEADING_NUMBER_RE.sub("", action)
144 lines.append(f" {i}. {clean}")
146 if summary.triage_suggestions:
147 lines.append("")
148 lines.append("Triage \u2014 Consider Suppressing:")
149 for suggestion in summary.triage_suggestions:
150 clean = LEADING_NUMBER_RE.sub("", suggestion)
151 lines.append(f" ~ {clean}")
153 if summary.estimated_effort:
154 lines.append("")
155 lines.append(f"Estimated effort: {summary.estimated_effort}")
157 if show_cost and summary.cost_estimate > 0:
158 lines.append("")
159 lines.append(f"AI cost: {format_cost(summary.cost_estimate)}")
161 lines.append("::endgroup::")
163 return "\n".join(lines)
166def render_summary_markdown(
167 summary: AISummary,
168 *,
169 show_cost: bool = True,
170) -> str:
171 """Render AI summary as Markdown with collapsible section.
173 Args:
174 summary: AI summary to render.
175 show_cost: Whether to show cost estimates.
177 Returns:
178 Markdown-formatted string.
179 """
180 if not summary.overview:
181 return ""
183 lines: list[str] = []
184 lines.append("### AI Summary")
185 lines.append("")
186 lines.append("<details>")
187 lines.append("<summary><b>Actionable insights</b></summary>")
188 lines.append("")
189 lines.append(summary.overview)
191 if summary.key_patterns:
192 lines.append("")
193 lines.append("**Key Patterns:**")
194 lines.append("")
195 for pattern in summary.key_patterns:
196 lines.append(f"- {pattern}")
198 if summary.priority_actions:
199 lines.append("")
200 lines.append("**Priority Actions:**")
201 lines.append("")
202 for i, action in enumerate(summary.priority_actions, 1):
203 clean = LEADING_NUMBER_RE.sub("", action)
204 lines.append(f"{i}. {clean}")
206 if summary.triage_suggestions:
207 lines.append("")
208 lines.append("**Triage \u2014 Consider Suppressing:**")
209 lines.append("")
210 for suggestion in summary.triage_suggestions:
211 clean = LEADING_NUMBER_RE.sub("", suggestion)
212 lines.append(f"- {clean}")
214 if summary.estimated_effort:
215 lines.append("")
216 lines.append(f"*Estimated effort: {summary.estimated_effort}*")
218 lines.append("")
219 lines.append("</details>")
221 if show_cost and summary.cost_estimate > 0:
222 lines.append("")
223 lines.append(f"*AI cost: {format_cost(summary.cost_estimate)}*")
225 return "\n".join(lines)
228def render_summary_annotations(summary: AISummary) -> str:
229 """Emit GitHub Actions annotation commands for AI summary insights.
231 Key patterns are emitted as ``::warning`` and priority actions as
232 ``::notice`` annotations so they surface in the Actions UI.
234 Args:
235 summary: AI summary to annotate.
237 Returns:
238 Newline-joined annotation commands, or empty string if the
239 summary has no actionable insights.
240 """
241 if not summary.overview:
242 return ""
244 lines: list[str] = []
246 for pattern in summary.key_patterns:
247 escaped = (
248 pattern.replace("%", "%25")
249 .replace("\r", "%0D")
250 .replace("\n", "%0A")
251 .replace("::", ":\u200b:")
252 )
253 lines.append(f"::warning title=AI Pattern::{escaped}")
255 for action in summary.priority_actions:
256 clean = LEADING_NUMBER_RE.sub("", action)
257 escaped = (
258 clean.replace("%", "%25")
259 .replace("\r", "%0D")
260 .replace("\n", "%0A")
261 .replace("::", ":\u200b:")
262 )
263 lines.append(f"::notice title=AI Priority::{escaped}")
265 return "\n".join(lines)
268def render_summary(
269 summary: AISummary,
270 *,
271 show_cost: bool = True,
272 output_format: str = "auto",
273) -> str:
274 """Render summary using the appropriate format for the environment.
276 Args:
277 summary: AI summary to render.
278 show_cost: Whether to show cost estimates.
279 output_format: Output format -- ``"auto"`` (default) selects
280 terminal or GitHub Actions based on environment,
281 ``"markdown"`` uses Markdown with collapsible section.
283 Returns:
284 Formatted summary string.
285 """
286 if output_format == "markdown":
287 return render_summary_markdown(summary, show_cost=show_cost)
288 if is_github_actions():
289 return render_summary_github(summary, show_cost=show_cost)
290 return render_summary_terminal(summary, show_cost=show_cost)