Skip to content

feat(formatters): render all terminal output via marked-terminal#297

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

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

Conversation

@BYK
Copy link
Member

@BYK BYK commented Feb 26, 2026

Summary

  • Add marked-terminal as the rendering engine for all human-readable CLI output — detail views for issues, events, orgs, projects, logs, traces, and Seer output now build markdown internally and render through marked-terminal
  • All detail sections use Unicode box-drawing tables (| **Label** | value | pattern) and styled headings; Seer root cause / solution output uses fenced code blocks
  • Streaming list output (formatLogRow, formatTraceRow) stays row-by-row for compatibility with follow/watch polling loops

Changes

  • Add src/lib/formatters/markdown.ts — central renderMarkdown() utility with Sentinel color palette
  • Migrate all detail formatters from string[]string (rendered markdown)
  • Remove divider() and formatTable() dead code from human.ts
  • Fix SDK name escaping in log.ts (underscores/dashes in names like sentry.python were consumed by markdown emphasis rules — now wrapped in code spans)
  • Update all formatter tests for the new string return type using content-based .toContain() checks

@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 via marked-terminal by BYK in #297

Bug Fixes 🐛

  • (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

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Codecov Results 📊

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

All tests are passing successfully.

✅ Patch coverage is 97.34%. Project has 3202 uncovered lines.
✅ Project coverage is 79.05%. Comparing base (base) to head (head).

Files with missing lines (13)
File Patch % Lines
plan.ts 19.47% ⚠️ 153 Missing
list.ts 30.28% ⚠️ 152 Missing
view.ts 26.67% ⚠️ 88 Missing
view.ts 41.50% ⚠️ 86 Missing
view.ts 68.04% ⚠️ 70 Missing
explain.ts 33.73% ⚠️ 55 Missing
human.ts 94.67% ⚠️ 48 Missing
view.ts 88.24% ⚠️ 26 Missing
view.ts 94.93% ⚠️ 7 Missing
output.ts 89.47% ⚠️ 2 Missing
seer.ts 98.46% ⚠️ 2 Missing
list.ts 98.99% ⚠️ 1 Missing
trace.ts 99.17% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    76.42%    79.05%    +2.63%
==========================================
  Files          117       118        +1
  Lines        15298     15286       -12
  Branches         0         0         —
==========================================
+ Hits         11691     12084      +393
- Misses        3607      3202      -405
- 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 added 11 commits February 26, 2026 16:09
Add marked-terminal as the rendering engine for all human-readable CLI output.
All detail views (issue, event, org, project, log, trace, Seer) now build
markdown internally and render through marked-terminal, producing Unicode
box-drawing tables and styled headings. Streaming list output (log/trace rows)
stays row-by-row for compatibility with follow/watch modes.

- Add src/lib/formatters/markdown.ts with central renderMarkdown() utility
- Rewrite formatters to return rendered string instead of string[]
- Convert all detail sections to markdown tables (| **Label** | value | pattern)
- Convert Seer root cause and solution output to fenced code blocks
- Remove divider(), formatTable() dead code from human.ts
- Escape SDK names in code spans to prevent markdown transformation of underscores
- Update all formatter tests for new string return type and content-based checks
…ble cells

Add escapeMarkdownCell() helper to markdown.ts that escapes backslashes
first, then pipe characters — fixing CodeQL 'Incomplete string escaping'
alerts where the previous \| replacement left backslashes unescaped.
…var overrides

Gate all markdown rendering behind isPlainOutput(): skip marked.parse() and
return raw CommonMark when stdout is not a TTY. Override with env vars:
- SENTRY_PLAIN_OUTPUT=1/0 — explicit project-specific control (highest priority)
- NO_COLOR=1/0 — widely-supported standard (secondary)
- process.stdout.isTTY — auto-detect (fallback)

Both env vars treat '0', 'false', '' as falsy; everything else as truthy
(case-insensitive). SENTRY_PLAIN_OUTPUT takes precedence over NO_COLOR.

Add renderInlineMarkdown() using marked.parseInline() for inline-only
rendering of individual cell values without block wrapping.

Migrate streaming formatters to dual-mode output:
- formatLogRow / formatLogsHeader: plain emits markdown table rows/header
- formatTraceRow / formatTracesHeader: same

This means piping to a file produces valid CommonMark; live TTY sessions
get the existing ANSI-rendered output unchanged.
Markdown table cells use '**Slug**' and '**DSN**' without trailing colons.
The E2E runner has no TTY so output is plain markdown — update assertions
to match.
- Extract LOG_TABLE_COLS and TRACE_TABLE_COLS module-level constants
- Add mdTableHeader() and divider() helpers to markdown.ts
- Reduce duplication in formatLogRow/formatTraceRow by building
  markdown cell values once, then branching only on isPlainOutput()
- Use middle truncation for log IDs (abc...123 vs abcdef...)
- Fix package.json repository URL to use git+ prefix
…n defs

- Add mdRow() helper: shared streaming row renderer used by both
  formatLogRow and formatTraceRow (eliminates duplicated isPlainOutput
  branching)
- Add mdKvTable() helper: builds key-value detail tables from
  [label, value] tuples (replaces manual string concatenation in
  formatLogDetails sections)
- Simplify mdTableHeader() column alignment: use ':' suffix convention
  (e.g. 'Duration:') instead of [name, 'right'] tuples
- Make formatLogsHeader/formatTracesHeader consistent: both modes now
  emit markdown table rows via mdRow() instead of diverging formats
…, table helpers

57 new tests covering:
- formatEventDetails with all conditional sections: stack traces,
  breadcrumbs, request, user/geo, environment, replay, tags, SDK
- formatSolution with steps, empty steps, markdown in descriptions
- formatLogTable and formatTraceTable batch rendering
- mdRow, mdKvTable, mdTableHeader, escapeMarkdownCell helpers
- NO_COLOR: follow no-color.org spec (any non-empty value disables color,
  including '0' and 'false'; only empty string leaves color on). Previously
  isTruthyEnv() was used which treated '0'/'false' as falsy, breaking
  interoperability with standard CI tooling.
- buildMarkdownTable: escape cell values via escapeMarkdownCell() so pipe
  and backslash characters in API-supplied names/URLs don't corrupt table
  structure (consistent with formatLogTable, formatTraceTable,
  buildBreadcrumbsMarkdown).
- formatIssueDetails: escape newlines in metadata.value blockquote with
  .replace(/\n/g, '\n> ') so multi-line error messages render correctly
  (same pattern already used in formatLogDetails).
- escapeMarkdownCell: replace newlines with space to prevent multi-line
  values from splitting a markdown table row across multiple lines
  (affects log messages, breadcrumb messages, tag values, any user content)
- formatLogTable / formatTraceTable: wire into the non-follow batch list
  paths (log/list.ts executeSingleFetch, trace/list.ts) — replaces the
  formatLogsHeader + formatLogRow loop so batch mode gets proper
  Unicode-bordered table rendering, streaming/follow mode still uses rows
- project/view.ts: restore muted() styling on the multi-project divider
  by importing and calling divider(60) instead of bare "─".repeat(60)
@BYK BYK force-pushed the feat/markdown-terminal-rendering branch from 1ad7523 to 2ff821a Compare February 26, 2026 16:12
- package.json: restore version to 0.14.0-dev.0, accidentally clobbered
  to 0.13.0-dev.0 during rebase conflict resolution
- human.ts: restore green()/yellow()/muted() colors to STATUS_ICONS and
  STATUS_LABELS, lost during the markdown migration; unknown statuses
  already used statusColor() so known statuses must also be colored to
  avoid the inconsistency
- markdown.ts mdRow: after renderInlineMarkdown(), replace residual |
  with U+2502 (│) so CommonMark backslash-escapes like \| that marked
  unescapes back to | don't visually break the pipe-delimited row layout
  in streaming follow mode
- trace.ts: extract buildTraceRowCells() private helper shared by both
  formatTraceRow (streaming) and formatTraceTable (batch) so cell
  formatting stays consistent; re-export formatTraceRow and
  formatTracesHeader to make them available for future streaming use cases
  and keep the existing tests valid
… in trace summary

- formatEventDetails (human.ts): wrap SDK name in backtick code span so
  names like sentry.python.aws_lambda don't render with markdown emphasis
  (same fix already applied to log.ts SDK field in a previous commit)
- formatTraceSummary (trace.ts): wrap rootTransaction through
  escapeMarkdownCell() — rootOp was already in backticks but rootTransaction
  was embedded raw, allowing pipe/underscore characters to break the table
  or produce unintended emphasis
- markdown.ts: add safeCodeSpan() helper for backtick-wrapped values
  inside table cells — replaces | with U+2502 and newlines with space
  since CommonMark code spans treat backslash as literal (not escape),
  making \| ineffective at preventing table splitting
- markdown.ts mdKvTable: apply escapeMarkdownCell() to every value so
  all key-value detail sections (org, project, log context, etc.) are
  safe against user-supplied | and newline characters
- human.ts: apply escapeMarkdownCell() to all bare user-supplied values
  in formatIssueDetails (platform, type, assignee, project name, release
  versions, permalink) and buildUserMarkdown (name, email, username, id,
  ip, geo location) and buildEnvironmentMarkdown (browser, OS, device)
- human.ts: use safeCodeSpan() for backtick-wrapped values (culprit,
  project slug, event location, trace ID) instead of raw template literal
  backtick wrapping
…cell sanitisation

mdKvTable unconditionally applied escapeMarkdownCell() to all values,
including those pre-formatted as code spans (e.g. `${logId}`,
`${codeFunction}`). escapeMarkdownCell() escapes backslashes first
(\→\\), which inside a CommonMark code span is literal, producing
double-backslashes in the rendered output (e.g. src\\lib\\parser.ts).

Replace escapeMarkdownCell with a new sanitizeKvCell() private helper
that only substitutes | with │ (U+2502 BOX DRAWINGS LIGHT VERTICAL,
visually identical, never a table delimiter) and newlines with a space.
This avoids:
- Double-escaping backslashes inside code spans
- Reintroducing CodeQL 'incomplete escaping' concern (no backslash
  escape chain is used at all — character substitution only)
- Table structure corruption from user-supplied pipe characters
The commit message for 2b7b534 described replacing residual | with U+2502
in mdRow's rendered path, but the actual code change to markdown.ts was
never committed — the mdRow implementation still called renderInlineMarkdown
without the post-render sanitisation.

Apply the fix: after renderInlineMarkdown(), replace all | (including those
that marked.parseInline unescapes from CommonMark \| backslash-escapes)
with │ (U+2502, BOX DRAWINGS LIGHT VERTICAL) so pipe characters in log
messages, transaction names, and other user content cannot visually corrupt
the pipe-delimited column structure in streaming TTY output.
…ove unused minWidth

- markdown.ts: add escapeMarkdownInline() which escapes \, *, _, `, [, ]
  — the characters that CommonMark processes as inline emphasis, strong,
  code spans, and links. Prevents Python/JS exception titles like
  'TypeError in __init__' from losing underscores (rendered as empty
  emphasis) when passed through renderMarkdown() in headings/blockquotes.
- human.ts formatIssueDetails: apply escapeMarkdownInline() to issue.title
  in the '## shortId: title' heading and to metadata.value in blockquotes
- human.ts formatEventDetails: apply escapeMarkdownInline() to the event
  header string used in '## header (`eventId`)' headings
- table.ts: remove minWidth from Column<T> interface — buildMarkdownTable
  never used it (auto-sized by cli-table3), so the property was dead
  configuration; remove corresponding dead minWidth values from repo/list.ts
  and team/list.ts callers
…ngling JSDoc

- log.ts: formatLogRow was using '**LEVEL**' (uniform bold, no color) for
  the severity cell, while formatLogTable correctly used formatSeverity()
  for per-level ANSI color (red/error, yellow/warning, cyan/info, muted/debug).
  Restore formatSeverity() in formatLogRow to keep streaming and batch modes
  consistent.
- table.ts: remove the dangling JSDoc comment '/** Minimum column width... */'
  left behind when the minWidth property was removed from Column<T> in the
  previous commit.
…sage blockquotes

- human.ts formatOrgDetails: escape org.name with escapeMarkdownCell() in
  the kv table row and escapeMarkdownInline() in the '## slug: name' heading
- human.ts formatProjectDetails: escape project.name, project.platform, and
  project.organization.name with escapeMarkdownCell(); use safeCodeSpan()
  for project.organization.slug; apply escapeMarkdownInline() to heading
- log.ts formatLogDetails: escape log.message with escapeMarkdownInline()
  before embedding in the blockquote — same pattern already applied to
  issue.metadata.value in human.ts; prevents __, *, and backtick chars in
  Python tracebacks or format strings from corrupting the rendered output
…es from breaking code spans

- markdown.ts safeCodeSpan: also replace ` with ˋ (U+02CB MODIFIER LETTER
  GRAVE ACCENT) — visually identical in monospace but never a code-span
  delimiter. Prevents JS exception messages like 'Unexpected token `' from
  prematurely closing the backtick-delimited code span.
- human.ts formatStackFrameMarkdown: use safeCodeSpan() for the full
  'at fn (file:line:col)' string instead of raw template-literal backticks
- human.ts formatExceptionValueMarkdown: use safeCodeSpan() for the
  'type: value' header instead of raw backticks
…in headings

- issue/list.ts: replace inline muted("─".repeat(n)) with divider(n) from
  markdown.ts — the helper already exists and is re-exported via the
  formatters index; inlining it duplicated the helper logic
- seer.ts buildRootCauseMarkdown: escape cause.description with
  escapeMarkdownInline() before embedding in the '### Cause #N: ...' heading
- seer.ts formatSolution: escape solution.data.one_line_summary with
  escapeMarkdownInline() before embedding in '**Summary:** ...' bold text
  — Seer AI-generated text can contain underscores, asterisks, and backticks
  which the markdown parser would interpret as emphasis or code spans
lines.push(...formatReproductionStep(step, i));
}
for (const step of cause.root_cause_reproduction) {
lines.push(`**${step.title}**`);
Copy link

Choose a reason for hiding this comment

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

Unescaped step titles break bold markdown formatting

Medium Severity

step.title is embedded directly inside **...** bold markers without escapeMarkdownInline(). In contrast, cause.description (line 111) and solution.data.one_line_summary (line 263) in the same file are properly escaped. If Seer generates a title containing **, _, or * (plausible for code-related descriptions like "Fix __init__ handling"), the bold delimiters would be corrupted, producing garbled rendered output.

Additional Locations (1)

Fix in Cursor Fix in Web

/** Function to format data as human-readable lines */
formatHuman: (data: T) => string[];
/** Function to format data as a rendered string or string array */
formatHuman: (data: T) => string | string[];
Copy link

Choose a reason for hiding this comment

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

Dead backward-compat shim in formatHuman return type

Low Severity

The formatHuman return type was widened from string[] to string | string[] and an Array.isArray() branch was added. Since all formatters were migrated to return string in this PR, the string[] path is dead code. The reviewer's stated preference is to fix callers directly rather than keep backward-compat shims.

Additional Locations (1)

Fix in Cursor Fix in Web

if (step) {
lines.push(` ${i + 1}. ${bold(step.title)}`);
lines.push(` ${muted(step.description)}`);
lines.push(`${i + 1}. **${step.title}**`);
Copy link

Choose a reason for hiding this comment

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

Unescaped step titles break markdown bold formatting

Medium Severity

step.title is embedded directly inside **...** bold markers without escaping via escapeMarkdownInline(). If the Seer API returns a title containing markdown special characters like *, _, or `, the bold formatting breaks and the output renders incorrectly. This is a regression — the old code used chalk's bold() which applied ANSI codes directly, making special characters inert. The same issue exists in buildRootCauseMarkdown at the other location.

Additional Locations (1)

Fix in Cursor Fix in Web

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.

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