Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/lib/formatters/plain-detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,29 @@ export function isPlainOutput(): boolean {
}

/**
* Strip ANSI escape sequences from a string.
* Strip ANSI/VT escape sequences from a string.
*
* Handles SGR codes (`\x1b[...m`) and OSC 8 terminal hyperlink sequences
* (`\x1b]8;;url\x07text\x1b]8;;\x07`).
* Covers the four escape-sequence families that can reach a terminal:
* - CSI (`\x1b[`): SGR colour codes, cursor movement, screen-clear, etc.
* - OSC (`\x1b]`): window-title changes, hyperlinks, etc.
* - DCS (`\x1bP`): device-control strings.
* - Two-character C1 ESC: single-byte sequences like `\x1bc` (terminal reset).
*/
export function stripAnsi(text: string): string {
Comment thread
sentry[bot] marked this conversation as resolved.
return (
text
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape detection requires matching \x1b and \x07
.replace(/\x1b\[[0-9;]*m/g, "")
// biome-ignore lint/suspicious/noControlCharactersInRegex: OSC 8 hyperlink sequences use \x1b and \x07
.replace(/\x1b\]8;;[^\x07]*\x07/g, "")
// CSI: \x1b[ + param bytes (0x30-0x3F) + intermediate bytes (0x20-0x2F) + final byte (0x40-0x7E)
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape detection requires matching \x1b
.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, "")
// OSC: \x1b] ... terminated by BEL (\x07) or ST (\x1b\)
// biome-ignore lint/suspicious/noControlCharactersInRegex: OSC sequences use \x1b and \x07
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
// DCS: \x1bP ... terminated by ST (\x1b\)
// biome-ignore lint/suspicious/noControlCharactersInRegex: DCS sequences use \x1b
.replace(/\x1bP[^\x1b]*\x1b\\/g, "")
// Two-character ESC sequences (C1: 0x40-0x5F, Fs: 0x60-0x7E), e.g. \x1bc (terminal reset).
// Applied last so CSI/OSC/DCS introducers (\x1b[, \x1b], \x1bP) are already stripped.
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC sequence detection requires matching \x1b
.replace(/\x1b[@-~]/g, "")
);
}
5 changes: 1 addition & 4 deletions src/lib/init/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ const FEEDBACK_COMMANDS: Record<InitFeedbackOutcome, string> = {
};

const FEEDBACK_COPY: Record<InitFeedbackOutcome, string[]> = {
success: [
"Nice, setup made it through.",
"Tell us what felt great or rough:",
],
success: ["Tell us what felt great or rough:"],
cancelled: [
"Sad to see setup stop. Was something going sideways?",
"Tell us so we can fix it:",
Expand Down
28 changes: 25 additions & 3 deletions src/lib/init/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
*/

import { terminalLink } from "../formatters/colors.js";
import { featureLabel } from "./clack-utils.js";
import { stripAnsi } from "../formatters/plain-detect.js";
import { featureLabel, sortFeatures } from "./clack-utils.js";
import {
EXIT_DEPENDENCY_INSTALL_FAILED,
EXIT_PLATFORM_NOT_DETECTED,
Expand All @@ -33,6 +34,22 @@
* appear.
*/
function buildSummary(output: WizardOutput): WizardSummary | null {
// Resolve blurbs first so the Features row can check the *resolved* length.
// If the agent returns blurbs with wrong IDs they all drop out here, and
// the Features row falls back to showing correctly.
const blurbMap = new Map(
(output.featureBlurbs ?? []).map(({ feature, blurb }) => [
feature,
stripAnsi(blurb),
])
Comment thread
betegon marked this conversation as resolved.
);
const featureBlurbs = sortFeatures(output.features ?? [])
.map((feature) => {
const blurb = blurbMap.get(feature);
return blurb ? { label: featureLabel(feature), blurb } : null;
})
.filter((b): b is { label: string; blurb: string } => b !== null);

const fields: WizardSummary["fields"] = [];

if (output.platform) {
Expand All @@ -41,7 +58,7 @@
if (output.projectDir) {
fields.push({ label: "Directory", value: output.projectDir });
}
if (output.features?.length) {
if (output.features?.length && !featureBlurbs.length) {

Check warning on line 61 in src/lib/init/formatters.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Partial blurb match silently drops unmatched features from summary

If the AI agent returns blurbs for only a subset of enabled features, the `featureBlurbs.length > 0` check suppresses the plain "Features" row while the unmatched features are filtered out — leaving them invisible in the summary.
fields.push({
label: "Features",
value: output.features.map(featureLabel).join(", "),
Expand All @@ -62,13 +79,18 @@

const changedFiles = output.changedFiles ?? [];

if (fields.length === 0 && changedFiles.length === 0) {
if (
fields.length === 0 &&
changedFiles.length === 0 &&
featureBlurbs.length === 0
) {
return null;
}

return {
fields,
...(changedFiles.length > 0 ? { changedFiles } : {}),
...(featureBlurbs.length > 0 ? { featureBlurbs } : {}),
};
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/init/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@
docsUrl?: string;
sentryProjectUrl?: string;
message?: string;
featureBlurbs?: Array<{ feature: string; blurb: string }>;

Check warning on line 232 in src/lib/init/types.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

[ECH-MY8] Partial blurb match silently drops unmatched features from summary (additional location)

If the AI agent returns blurbs for only a subset of enabled features, the `featureBlurbs.length > 0` check suppresses the plain "Features" row while the unmatched features are filtered out — leaving them invisible in the summary.
};

// Interactive payloads
Expand Down
22 changes: 22 additions & 0 deletions src/lib/init/ui/ink-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,28 @@ function SummaryPanel({
))}
</Box>
) : null}
{summary.featureBlurbs !== undefined &&
summary.featureBlurbs.length > 0 ? (
<Box flexDirection="column" flexShrink={0} marginTop={1}>
<Text bold color={MUTED}>
Here&apos;s what we set up
</Text>
{summary.featureBlurbs.map(({ label, blurb }) => (
<Box flexDirection="row" flexShrink={0} key={label}>
<Box flexShrink={0} width={22}>
<Text bold color={PRIMARY}>
{label}
</Text>
</Box>
<Box flexShrink={1}>
<Text color={MUTED} wrap="wrap">
{blurb}
</Text>
</Box>
</Box>
))}
</Box>
) : null}
Comment thread
cursor[bot] marked this conversation as resolved.
{summary.changedFiles !== undefined && summary.changedFiles.length > 0 ? (
<ChangedFilesTree files={summary.changedFiles} />
) : null}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/init/ui/ink-report.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import chalk from "chalk";
import { renderTextTable } from "../../formatters/text-table.js";
import { buildFileTree, flattenTree } from "./file-tree.js";
import type { WizardSummary } from "./types.js";

Expand Down Expand Up @@ -60,6 +61,20 @@ export function formatSuccessReport(
lines.push(` ${label} ${field.value}`);
}
}
if (summary?.featureBlurbs && summary.featureBlurbs.length > 0) {
lines.push("");
lines.push(` ${chalk.hex(REPORT_MUTED).bold("Here's what we set up")}`);
const tableRows = summary.featureBlurbs.map(({ label, blurb }) => [
chalk.bold(label),
chalk.hex(REPORT_MUTED)(blurb),
Comment thread
sentry-warden[bot] marked this conversation as resolved.
]);
const table = renderTextTable(["", ""], tableRows, {
shrinkable: [false, true],
});
for (const line of table.trimEnd().split("\n")) {
lines.push(` ${line}`);
}
}
if (summary?.changedFiles && summary.changedFiles.length > 0) {
lines.push("");
lines.push(` ${chalk.hex(REPORT_MUTED).bold("Changed files")}`);
Expand Down
21 changes: 20 additions & 1 deletion src/lib/init/ui/logging-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
renderInlineMarkdown,
renderMarkdown,
} from "../../formatters/markdown.js";
import { renderTextTable } from "../../formatters/text-table.js";
import { formatFeedbackHint, type InitFeedbackOutcome } from "../feedback.js";
import { buildFileTree, flattenTree } from "./file-tree.js";
import type {
Expand Down Expand Up @@ -94,7 +95,11 @@ export class LoggingUI implements WizardUI {
}

summary(summary: WizardSummary): void {
if (summary.fields.length === 0 && !summary.changedFiles?.length) {
if (
summary.fields.length === 0 &&
!summary.changedFiles?.length &&
!summary.featureBlurbs?.length
) {
return;
}
// Compact two-column key/value listing — one line per field. The
Expand All @@ -109,6 +114,20 @@ export class LoggingUI implements WizardUI {
const padded = field.label.padEnd(labelWidth);
this.writeLine(this.stdout, ` ${padded} ${field.value}`);
}
if (summary.featureBlurbs && summary.featureBlurbs.length > 0) {
this.writeLine(this.stdout, "");
this.writeLine(this.stdout, " Here's what we set up");
const tableRows = summary.featureBlurbs.map(({ label, blurb }) => [
label,
blurb,
]);
const table = renderTextTable(["", ""], tableRows, {
shrinkable: [false, true],
});
for (const line of table.trimEnd().split("\n")) {
this.writeLine(this.stdout, ` ${line}`);
}
}
if (summary.changedFiles && summary.changedFiles.length > 0) {
this.writeLine(this.stdout, "");
this.writeLine(this.stdout, " Changed files:");
Expand Down
2 changes: 2 additions & 0 deletions src/lib/init/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export type WizardSummary = {
fields: { label: string; value: string }[];
/** Optional list of files the wizard added/edited/removed. */
changedFiles?: { action: string; path: string }[];
/** AI-generated per-feature blurbs personalised to the analysed project. */
featureBlurbs?: { label: string; blurb: string }[];
};

/**
Expand Down
1 change: 0 additions & 1 deletion test/lib/init/feedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ describe("formatFeedbackHint", () => {
test("maps init outcomes to copy-paste feedback commands", () => {
expect(formatFeedbackHint("success")).toBe(
[
"Nice, setup made it through.",
"Tell us what felt great or rough:",
'$ sentry cli feedback "sentry init worked well"',
].join("\n")
Expand Down
Loading
Loading