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 📊 Comparison with Base Branch
All tests are passing successfully. ✅ Patch coverage is 91.18%. 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 16218 +642
Branches 0 0 —
==========================================
+ Hits 12002 12983 +981
- 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
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.
Move escapeMarkdownCell responsibility from buildMarkdownTable to individual column value functions. This preserves intentional markdown (links in SHORT ID, code spans in SLUG) while escaping user data (NAME, TITLE, URL) at the source. Columns that produce markdown-safe content (slugs, enums, counts) don't need escaping.
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.
src/lib/formatters/human.ts
Outdated
| export function formatRelativeTime(dateString: string | undefined): string { | ||
| if (!dateString) { | ||
| return muted("—").padEnd(10); | ||
| return muted("—"); |
There was a problem hiding this comment.
ANSI codes leak into plain/piped output
High Severity
formatRelativeTime returns muted("—") for undefined dates, which applies ANSI escape codes via chalk. In the new markdown-first architecture, all cell values pass through stripColorTags() in plain mode, but that function only strips custom HTML-like <red>…</red> color tags — not raw ANSI escape sequences. So the ANSI codes from muted() leak into piped/CI/non-TTY output. Every other formatter in this PR uses colorTag() for colors, which stripColorTags() can handle. This call needs to use colorTag("muted", "—") instead.
src/lib/formatters/human.ts
Outdated
| }, | ||
| { | ||
| header: "TITLE", | ||
| value: ({ issue }) => escapeMarkdownCell(issue.title), |
There was a problem hiding this comment.
Issue titles with underscores render as markdown emphasis
Medium Severity
The TITLE column uses escapeMarkdownCell(issue.title) which does not escape _ or *. Since cell values pass through renderInlineMarkdown in TTY mode, issue titles containing paired underscores (common in Python — __init__, __main__, __str__) are rendered as bold/italic emphasis instead of literal text. Using escapeMarkdownInline or adding emphasis-character escaping to this cell value would prevent misrendered titles.
…r titles
Replace muted("—") with colorTag("muted", "—") in formatRelativeTime
so stripColorTags handles it in plain mode — raw ANSI from chalk leaks
through since stripColorTags only strips <tag> pairs.
Use escapeMarkdownInline for issue titles instead of escapeMarkdownCell
to escape _, *, and backticks — prevents __init__ rendering as emphasis
in TTY mode.
| { | ||
| header: "TITLE", | ||
| value: ({ issue }) => escapeMarkdownInline(issue.title), | ||
| } |
There was a problem hiding this comment.
Bug: The TITLE column uses escapeMarkdownInline, which doesn't escape pipe characters (|). This corrupts the markdown table layout when an issue title contains a pipe in plain output mode.
Severity: MEDIUM
Suggested Fix
In src/lib/formatters/human.ts, change the TITLE column definition to use escapeMarkdownCell instead of escapeMarkdownInline. This will correctly escape pipe characters and other structural table characters, preventing layout corruption.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/lib/formatters/human.ts#L478-L481
Potential issue: In `src/lib/formatters/human.ts`, the `TITLE` column's value is
processed by `escapeMarkdownInline`. This function does not escape pipe characters
(`|`). When the CLI is run in plain output mode (non-TTY), `buildMarkdownTable`
constructs a markdown table by joining cell values with ` | `. If an issue title
contains a pipe, it will be treated as a column delimiter, corrupting the table's
structure by adding an extra, unintended column. This is a realistic scenario as error
messages often contain pipes.
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.
formatLogTable and formatTraceTable already return strings ending with a newline from renderTextTable. The template literal wrapper added a second one, creating two blank lines before the footer.



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