Skip to content

feat(formatters): render all terminal output as markdown#297

Open
BYK wants to merge 50 commits intomainfrom
feat/markdown-terminal-rendering
Open

feat(formatters): render all terminal output as markdown#297
BYK wants to merge 50 commits intomainfrom
feat/markdown-terminal-rendering

Conversation

@BYK
Copy link
Member

@BYK BYK commented Feb 26, 2026

Summary

  • Replace all ad-hoc ANSI/chalk formatting with a markdown-first pipeline: formatters build CommonMark strings internally, which are rendered through a custom marked-terminal renderer in TTY mode or emitted as raw markdown in non-TTY/piped mode
  • Plain output mode: auto-detected via process.stdout.isTTY; overridable with SENTRY_PLAIN_OUTPUT or NO_COLOR. TTY → styled Unicode tables + ANSI colors. Non-TTY → raw CommonMark (great for piping, CI, AI agents)
  • Issue list SHORT IDs are markdown links rendered as OSC 8 terminal hyperlinks (clickable in iTerm2, VS Code, Windows Terminal, etc.) with alias characters bold-highlighted (e.g. CLI-WEBSITE-4)
  • Seer output (root cause analysis, fix plans) renders fenced code blocks with syntax highlighting and structured heading hierarchy
image

Key changes

  • Add src/lib/formatters/markdown.tsrenderMarkdown(), renderInlineMarkdown(), isPlainOutput(), escapeMarkdownCell(), mdRow(), mdKvTable(), mdTableHeader(), divider() helpers
  • All detail formatters (formatIssueDetails, formatEventDetails, formatOrgDetails, formatProjectDetails, formatLogDetails, formatTraceSummary, formatRootCauseList, formatSolution) return rendered string instead of string[]
  • writeTable() processes cell values through renderInlineMarkdown() so **bold** and [text](url) in cells render as styled/clickable text; markdown links become OSC 8 hyperlinks
  • formatShortId() returns markdown bold (**text**) instead of ANSI for alias highlighting — composable with link syntax
  • Streaming row formatters (formatLogRow, formatTraceRow) are dual-mode: padded ANSI text in TTY, raw markdown table rows in plain mode
  • Fix SDK name/exception value/org name escaping so special characters (_, `, |, \) don't break markdown rendering

@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (formatters) Render all terminal output as markdown by BYK in #297
  • (issue-list) Global limit with fair distribution, compound cursor, and richer progress by BYK in #306

Bug Fixes 🐛

Api

  • Use limit param for issues endpoint page size by BYK in #309
  • Auto-correct ':' to '=' in --field values with a warning by BYK in #302

Other

  • (ci) Generate JUnit XML to silence codecov-action warnings by BYK in #300
  • (nightly) Push to GHCR from artifacts dir so layer titles are bare filenames by BYK in #301
  • (test) Handle 0/-0 in getComparator anti-symmetry property test by BYK in #308

Internal Changes 🔧

  • (api) Wire listIssuesPaginated through @sentry/api SDK for type safety by BYK in #310

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Codecov Results 📊

2268 passed | Total: 2268 | Pass Rate: 100% | Execution Time: 0ms

All tests are passing successfully.

✅ Patch coverage is 91.16%. Project has 3235 uncovered lines.
✅ Project coverage is 80.05%. Comparing base (base) to head (head).

Files with missing lines (20)
File Patch % Lines
list.ts 27.80% ⚠️ 174 Missing
plan.ts 19.47% ⚠️ 153 Missing
list.ts 87.19% ⚠️ 92 Missing
view.ts 26.67% ⚠️ 88 Missing
view.ts 41.50% ⚠️ 86 Missing
list.ts 23.66% ⚠️ 71 Missing
view.ts 68.04% ⚠️ 70 Missing
list.ts 85.64% ⚠️ 59 Missing
explain.ts 33.73% ⚠️ 55 Missing
markdown.ts 88.82% ⚠️ 36 Missing
human.ts 96.14% ⚠️ 31 Missing
view.ts 88.24% ⚠️ 26 Missing
table.ts 61.54% ⚠️ 15 Missing
trace.ts 92.74% ⚠️ 9 Missing
view.ts 94.93% ⚠️ 7 Missing
log.ts 96.79% ⚠️ 5 Missing
colors.ts 96.55% ⚠️ 2 Missing
output.ts 89.47% ⚠️ 2 Missing
seer.ts 98.46% ⚠️ 2 Missing
list.ts 98.99% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    77.05%    80.05%       +3%
==========================================
  Files          117       120        +3
  Lines        15576     16216      +640
  Branches         0         0         —
==========================================
+ Hits         12002     12981      +979
- Misses        3574      3235      -339
- Partials         0         0         —

Generated by Codecov Action

@BYK
Copy link
Member Author

BYK commented Feb 26, 2026

Re: merging human.ts and markdown.ts (comment)

Good point — they do overlap in responsibility. markdown.ts is the rendering engine layer (marked-terminal config, renderMarkdown(), isPlainOutput(), escapeMarkdownCell(), mdTableHeader(), divider()). human.ts is the content/layout layer that builds entity-specific markdown strings (issues, events, orgs, projects) and pipes them through the engine.

Merging them would create a ~1600-line file. I think the current split works well: markdown.ts is entity-agnostic utilities, human.ts is Sentry-entity-specific formatting. Similar to how colors.ts defines color functions but human.ts decides which fields get which colors.

That said, if you feel strongly about it I can merge — just want to flag the file size concern.

@BYK BYK marked this pull request as ready for review February 26, 2026 14:45
@BYK BYK force-pushed the feat/markdown-terminal-rendering branch from 1ad7523 to 2ff821a Compare February 26, 2026 16:12
BYK added 9 commits February 28, 2026 17:18
…ue status

Replace direct chalk/ANSI color calls in markdown-emitting contexts with
semantic <tag>text</tag> color tags handled by the custom renderer.

- Add COLOR_TAGS map and colorTag() helper to markdown.ts
- renderInline() now handles html tokens: <red>text</red> → chalk.hex(red)(text)
- renderHtmlToken() extracted as module-level helper to stay within complexity limit
- log.ts: SEVERITY_COLORS (chalk fns) → SEVERITY_TAGS (tag name strings)
- human.ts: levelColor/fixabilityColor/statusColor/green/yellow/muted calls in
  markdown contexts → colorTag() with LEVEL_TAGS / FIXABILITY_TAGS maps
- In plain (non-TTY) output mode, color tags are stripped leaving bare text
- tests: strip <tag> markers alongside ANSI in content assertions
…ortIdWithAlias

The o1/d cross-org collision alias format was dropped during the markdown
refactor. formatShortIdWithAlias now extracts the project part after '/'
before matching against short ID segments, restoring highlight for e.g.
DASHBOARD-A3 with alias 'o1/d' → CLI-**D**ASHBOARD-**A3**.
…, colors, table plain mode

New test files:
- text-table.test.ts: 27 tests covering renderTextTable (border styles,
  alignment, column fitting proportional/balanced, cell wrapping,
  ANSI-aware width, header separator, multi-column structure)
- colors.test.ts: 15 tests for statusColor, levelColor, fixabilityColor,
  terminalLink

Expanded existing files:
- markdown.test.ts: +33 tests for colorTag, escapeMarkdownInline,
  safeCodeSpan, divider, renderMarkdown blocks (headings, code, blockquote,
  lists, hr, tables), renderInlineMarkdown tokens (italic, links,
  strikethrough, color tags, unknown tags)
- table.test.ts: +2 tests for plain-mode markdown table output

Fix: renderInline now handles paired color tags that marked emits as
separate html tokens (<red>, text, </red>) by buffering inner tokens
until the close tag and applying the color function.
The issue list table had each row spanning 2+ lines because the balanced
column fitter shrunk the SHORT ID column below the text width when
terminals were narrower than ~114 columns.

Changes:
- Add truncate option to TextTableOptions: cells clip to 1 line with
  '…' instead of wrapping to multiple lines
- Add minWidths option to TextTableOptions: per-column minimum content
  widths that the fitters respect as floors during shrinking
- Add minWidth and truncate fields to Column<T> in table.ts, passed
  through to renderTextTable via writeTable
- Set minWidth: 20 on the SHORT ID column in writeIssueTable to prevent
  short IDs from being squeezed; TITLE absorbs the shrink instead
- Enable truncate: true for the issue list table
…ncation

SHORT ID values should never be truncated or shrunk — they are the
primary identifier users copy for 'sentry issue view <ID>'. TITLE
should show the full text, wrapping to multiple lines if needed.

Replace the blanket truncate: true on the issue table with a
shrinkable column option:

- Add shrinkable?: boolean[] to TextTableOptions and Column<T>
- Non-shrinkable columns keep their intrinsic (measured) width
- fitColumns separates fixed vs elastic columns: fixed columns are
  excluded from the fitting algorithm, elastic ones share the rest
- Set shrinkable: false on the SHORT ID column in writeIssueTable
- Remove truncate: true from writeIssueTable (TITLE wraps naturally)
- Fix terminalLink OSC 8 hyperlinks: add missing \x1b ESC prefix and \x07 BEL terminator
- Fix escapeMarkdownCell/escapeMarkdownInline: escape < and > to &lt;/&gt; so user content with angle brackets (e.g. 'Expected <string>') is not silently dropped by marked's HTML token renderer
- Strip color tags (<red>text</red>) in plain output mode so they don't leak as literal markup when piped/redirected
- Fix mdKvTable: escape backslashes before replacing pipes, matching escapeMarkdownCell behavior
- Fix streaming headers (formatLogsHeader/formatTracesHeader): emit proper markdown table header+separator in plain mode instead of Unicode divider
- Escape issue titles in writeIssueTable with escapeMarkdownInline to prevent underscores/asterisks from rendering as emphasis
- Differentiate h1/h2 (bold cyan) from h3+ (plain cyan) heading rendering instead of identical branches
- Remove orphaned JSDoc comment for deleted writeIssueRows function
- Type SEVERITY_TAGS precisely to eliminate unsafe cast at call sites
- Fix .trim() on formatLogTable severity: use formatSeverityLabel (no padding) instead of trimming inside color tag
- Update tests to match corrected behavior
- Remove duplicate Long-term Knowledge section in AGENTS.md (131 lines
  of redundant lore entries, including 2 duplicate preference entries
  with different IDs)
- Export COLORS from colors.ts and import in markdown.ts instead of
  maintaining a separate copy
…ets in mdKvTable

- mdRow: strip color tags from cells in plain mode so <red>ERROR</red>
  becomes just ERROR in piped/CI output
- buildMarkdownTable: strip color tags before escaping cells to prevent
  double-encoding (<red> -> &lt;red&gt;) in plain mode
- mdKvTable: escape < and > to &lt;/&gt; so user content like
  'Expected <string>' isn't silently dropped by the markdown parser
- Export stripColorTags for use in table.ts
@BYK BYK force-pushed the feat/markdown-terminal-rendering branch from f46a348 to 6afd84b Compare February 28, 2026 17:22
…Table/mdRow

Migrate 11 manual markdown table locations to use shared helpers:
- 6 KV tables in human.ts + 1 in trace.ts now use mdKvTable()
- 3 data table rows in human.ts, trace.ts, log.ts now use mdRow()
- 1 data table header in human.ts now uses mdTableHeader()

Simplify mdKvTable() escaping: only replace pipes and newlines
(structural safety). Content escaping is the caller's responsibility,
which allows values with intentional markdown (colorTag, safeCodeSpan,
backtick spans) to pass through unmangled.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

… output

Add StreamingTable class to text-table.ts that renders incrementally:
header (top border + column names + separator), row-by-row with side
borders, and footer (bottom border) on stream end.

Log streaming (--follow) now uses bordered tables in TTY mode:
- createLogStreamingTable() factory with pre-configured column hints
- SIGINT handler prints bottom border before exit
- Plain mode (non-TTY) still emits raw markdown rows for pipe safety

Trace streaming gets createTraceStreamingTable() factory (not yet wired
to command — traces don't have --follow yet, but the factory is ready).

Extract buildLogRowCells() and export buildTraceRowCells() so both
streaming (StreamingTable.row) and batch (formatLogTable/formatTraceTable)
paths share the same cell-building logic.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

…ch table double-render

- Escape firstReleaseVersion/lastReleaseVersion in issue firstSeen/lastSeen
  cells via escapeMarkdownCell() to prevent pipe/backslash chars breaking
  markdown table structure
- Escape rootTransaction in formatTraceSummary for the same reason
- Fix formatLogTable and formatTraceTable which were passing mdRow output
  (already ANSI-rendered in TTY mode) through renderMarkdown() a second time,
  mangling the table. Both functions now use renderTextTable() directly in
  TTY mode and mdRow/mdTableHeader in plain mode (same pattern as writeTable)
- Remove stale @param isMultiProject from writeListHeader JSDoc
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reorder the two .replace() calls in the test helper so that color
tags (<red>, </red>) are removed first. This prevents CodeQL's
'incomplete multi-character sanitization' alert where ANSI code
removal could theoretically join fragments into tag-like sequences.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

BYK added 2 commits February 28, 2026 22:00
… ID column

Replace markdown **bold** syntax with chalk boldUnderline() for alias
highlighting in formatShortId and formatShortIdWithAlias. The markdown
approach only produced bold text; the original used bold+underline via
chalk to make alias indicators visually distinct. ANSI codes survive
the table rendering pipeline and display correctly in TTY mode.
…ANSI

Use the existing colorTag system with a new 'bu' (bold+underline)
tag instead of direct chalk calls. This keeps alias indicators as
markdown-compatible content that strips cleanly in plain mode and
renders through the custom markdown renderer in TTY mode.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

… code

Escape user-facing values (org.name, project.name, assignee name,
release versions) with escapeMarkdownInline() before passing to
mdKvTable, as its contract requires callers to handle content
escaping. Remove unused createTraceStreamingTable, TRACE_HINT_ROWS,
and StreamingTableOptions import — trace list has no streaming mode.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Apply renderInlineMarkdown() to each cell in the log streaming path
so that color tags (<red>ERROR</red>) and code spans are rendered to
ANSI instead of appearing as literal text.

Hoist the COLOR_TAG_RE regex to module scope so stripColorTags()
doesn't recompile it on every invocation.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Remove escapeMarkdownInline from the TITLE column value function in
writeIssueTable — column values should return raw text, not pre-escaped
markdown. Instead, expand escapeMarkdownCell to also escape inline
emphasis chars (*, _, `, [, ]) so the plain-mode path in
buildMarkdownTable produces safe CommonMark without double-escaping
backslashes.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Plain-mode table escaping destroys intentional markdown links
    • Moved user data escaping to column definitions (TITLE, NAME, URL fields) and removed blanket escaping from buildMarkdownTable, preserving intentional markdown links in SHORT ID column.

Create PR

Or push these changes by commenting:

@cursor push 137e130db6
Preview (137e130db6)
diff --git a/src/commands/org/list.ts b/src/commands/org/list.ts
--- a/src/commands/org/list.ts
+++ b/src/commands/org/list.ts
@@ -10,6 +10,7 @@
 import { DEFAULT_SENTRY_HOST } from "../../lib/constants.js";
 import { getAllOrgRegions } from "../../lib/db/regions.js";
 import { writeFooter, writeJson } from "../../lib/formatters/index.js";
+import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
 import { type Column, writeTable } from "../../lib/formatters/table.js";
 import { buildListLimitFlag, LIST_JSON_FLAG } from "../../lib/list-command.js";
 
@@ -108,7 +109,7 @@
       ...(showRegion
         ? [{ header: "REGION", value: (r: OrgRow) => r.region ?? "" }]
         : []),
-      { header: "NAME", value: (r) => r.name },
+      { header: "NAME", value: (r) => escapeMarkdownCell(r.name) },
     ];
 
     writeTable(stdout, rows, columns);

diff --git a/src/commands/project/list.ts b/src/commands/project/list.ts
--- a/src/commands/project/list.ts
+++ b/src/commands/project/list.ts
@@ -32,6 +32,7 @@
 } from "../../lib/db/pagination.js";
 import { AuthError, ContextError } from "../../lib/errors.js";
 import { writeFooter, writeJson } from "../../lib/formatters/index.js";
+import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
 import { type Column, writeTable } from "../../lib/formatters/table.js";
 import {
   buildListCommand,
@@ -229,7 +230,7 @@
 const PROJECT_COLUMNS: Column<ProjectWithOrg>[] = [
   { header: "ORG", value: (p) => p.orgSlug || "" },
   { header: "PROJECT", value: (p) => p.slug },
-  { header: "NAME", value: (p) => p.name },
+  { header: "NAME", value: (p) => escapeMarkdownCell(p.name) },
   { header: "PLATFORM", value: (p) => p.platform || "" },
 ];
 

diff --git a/src/commands/repo/list.ts b/src/commands/repo/list.ts
--- a/src/commands/repo/list.ts
+++ b/src/commands/repo/list.ts
@@ -14,6 +14,7 @@
   listRepositories,
   listRepositoriesPaginated,
 } from "../../lib/api-client.js";
+import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
 import { type Column, writeTable } from "../../lib/formatters/table.js";
 import {
   buildOrgListCommand,
@@ -31,10 +32,10 @@
 /** Column definitions for the repository table. */
 const REPO_COLUMNS: Column<RepositoryWithOrg>[] = [
   { header: "ORG", value: (r) => r.orgSlug || "" },
-  { header: "NAME", value: (r) => r.name },
+  { header: "NAME", value: (r) => escapeMarkdownCell(r.name) },
   { header: "PROVIDER", value: (r) => r.provider.name },
   { header: "STATUS", value: (r) => r.status },
-  { header: "URL", value: (r) => r.url || "" },
+  { header: "URL", value: (r) => escapeMarkdownCell(r.url || "") },
 ];
 
 /** Shared config that plugs into the org-list framework. */

diff --git a/src/commands/team/list.ts b/src/commands/team/list.ts
--- a/src/commands/team/list.ts
+++ b/src/commands/team/list.ts
@@ -15,6 +15,7 @@
   listTeams,
   listTeamsPaginated,
 } from "../../lib/api-client.js";
+import { escapeMarkdownCell } from "../../lib/formatters/markdown.js";
 import { type Column, writeTable } from "../../lib/formatters/table.js";
 import {
   buildOrgListCommand,
@@ -33,7 +34,7 @@
 const TEAM_COLUMNS: Column<TeamWithOrg>[] = [
   { header: "ORG", value: (t) => t.orgSlug || "" },
   { header: "SLUG", value: (t) => t.slug },
-  { header: "NAME", value: (t) => t.name },
+  { header: "NAME", value: (t) => escapeMarkdownCell(t.name) },
   {
     header: "MEMBERS",
     value: (t) => String(t.memberCount ?? ""),

diff --git a/src/lib/formatters/human.ts b/src/lib/formatters/human.ts
--- a/src/lib/formatters/human.ts
+++ b/src/lib/formatters/human.ts
@@ -477,7 +477,7 @@
     },
     {
       header: "TITLE",
-      value: ({ issue }) => issue.title,
+      value: ({ issue }) => escapeMarkdownCell(issue.title),
     }
   );
 

diff --git a/src/lib/formatters/table.ts b/src/lib/formatters/table.ts
--- a/src/lib/formatters/table.ts
+++ b/src/lib/formatters/table.ts
@@ -55,7 +55,7 @@
   const rows = items
     .map(
       (item) =>
-        `| ${columns.map((c) => escapeMarkdownCell(stripColorTags(c.value(item)))).join(" | ")} |`
+        `| ${columns.map((c) => stripColorTags(c.value(item))).join(" | ")} |`
     )
     .join("\n");
   return `${header}\n${separator}\n${rows}`;
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Revert [, ], and backtick escaping from escapeMarkdownCell — these
broke intentional markdown links ([SHORT-ID](permalink)) in the
issue table's SHORT ID column. Keep only _ and * escaping (for
issue titles) alongside the original structural escapes (|, \, <, >).
Column values are raw text; escapeMarkdownCell is the single
escaping pass in buildMarkdownTable.
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