feat(formatters): render all terminal output as markdown#297
feat(formatters): render all terminal output as markdown#297
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛Api
Other
Internal Changes 🔧
🤖 This preview updates automatically when you update the PR. |
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. Files with missing lines (20)
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 |
|
Re: merging Good point — they do overlap in responsibility. Merging them would create a ~1600-line file. I think the current split works well: That said, if you feel strongly about it I can merge — just want to flag the file size concern. |
1ad7523 to
2ff821a
Compare
…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 </> 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> -> <red>) in plain mode - mdKvTable: escape < and > to </> so user content like 'Expected <string>' isn't silently dropped by the markdown parser - Export stripColorTags for use in table.ts
f46a348 to
6afd84b
Compare
…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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
… 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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}`;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.

Summary
process.stdout.isTTY; overridable withSENTRY_PLAIN_OUTPUTorNO_COLOR. TTY → styled Unicode tables + ANSI colors. Non-TTY → raw CommonMark (great for piping, CI, AI agents)CLI-WEBSITE-4)Key changes
src/lib/formatters/markdown.ts—renderMarkdown(),renderInlineMarkdown(),isPlainOutput(),escapeMarkdownCell(),mdRow(),mdKvTable(),mdTableHeader(),divider()helpersformatIssueDetails,formatEventDetails,formatOrgDetails,formatProjectDetails,formatLogDetails,formatTraceSummary,formatRootCauseList,formatSolution) return renderedstringinstead ofstring[]writeTable()processes cell values throughrenderInlineMarkdown()so**bold**and[text](url)in cells render as styled/clickable text; markdown links become OSC 8 hyperlinksformatShortId()returns markdown bold (**text**) instead of ANSI for alias highlighting — composable with link syntaxformatLogRow,formatTraceRow) are dual-mode: padded ANSI text in TTY, raw markdown table rows in plain mode_,`,|,\) don't break markdown rendering