Skip to content

Commit 510047b

Browse files
betegonclaude
andcommitted
refactor(init): use renderInlineMarkdown for spinner messages
Pipe spinner messages through the markdown rendering pipeline so file names, commands, and project names display as styled inline code in the terminal. Update truncateForTerminal to handle ANSI escape sequences when measuring visible width. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 79e72c1 commit 510047b

File tree

2 files changed

+45
-24
lines changed

2 files changed

+45
-24
lines changed

src/lib/init/wizard-runner.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { CLI_VERSION } from "../constants.js";
2525
import { getAuthToken } from "../db/auth.js";
2626
import { WizardError } from "../errors.js";
2727
import { terminalLink } from "../formatters/colors.js";
28+
import { renderInlineMarkdown } from "../formatters/markdown.js";
29+
import { stripAnsi } from "../formatters/plain-detect.js";
2830
import { resolveOrCreateTeam } from "../resolve-team.js";
2931
import { slugify } from "../utils.js";
3032
import {
@@ -86,10 +88,28 @@ function nextPhase(
8688
function truncateForTerminal(message: string): string {
8789
// Reserve space for spinner (2 chars) + some padding
8890
const maxWidth = (process.stdout.columns || 80) - 4;
89-
if (message.length <= maxWidth) {
91+
if (stripAnsi(message).length <= maxWidth) {
9092
return message;
9193
}
92-
return `${message.slice(0, maxWidth - 1)}…`;
94+
// Truncate by visible width, preserving ANSI sequences
95+
let visible = 0;
96+
let i = 0;
97+
let result = "";
98+
while (i < message.length && visible < maxWidth - 1) {
99+
if (message[i] === "\x1b") {
100+
const end = message.indexOf("m", i);
101+
if (end !== -1) {
102+
result += message.slice(i, end + 1);
103+
i = end + 1;
104+
continue;
105+
}
106+
}
107+
result += message[i];
108+
visible += 1;
109+
i += 1;
110+
}
111+
// Close any open ANSI sequences with a reset
112+
return `${result}\x1b[0m…`;
93113
}
94114

95115
/**
@@ -112,7 +132,7 @@ export function describeLocalOp(payload: LocalOpPayload): string {
112132
const first = patches[0];
113133
if (patches.length === 1 && first) {
114134
const verb = patchActionVerb(first.action);
115-
return `${verb} ${basename(first.path)}...`;
135+
return `${verb} \`${basename(first.path)}\`...`;
116136
}
117137
const counts = patchActionCounts(patches);
118138
return `Applying ${patches.length} file changes (${counts})...`;
@@ -121,14 +141,14 @@ export function describeLocalOp(payload: LocalOpPayload): string {
121141
const cmds = payload.params.commands;
122142
const first = cmds[0];
123143
if (cmds.length === 1 && first) {
124-
return `Running ${first}...`;
144+
return `Running \`${first}\`...`;
125145
}
126-
return `Running ${cmds.length} commands (${first ?? "..."}, ...)...`;
146+
return `Running ${cmds.length} commands (\`${first ?? "..."}\`, ...)...`;
127147
}
128148
case "list-dir":
129149
return "Listing directory...";
130150
case "create-sentry-project":
131-
return `Creating project "${payload.params.name}" (${payload.params.platform})...`;
151+
return `Creating project \`${payload.params.name}\` (${payload.params.platform})...`;
132152
case "detect-sentry":
133153
return "Checking for existing Sentry setup...";
134154
default:
@@ -144,12 +164,12 @@ function describeFilePaths(verb: string, paths: string[]): string {
144164
return `${verb} files...`;
145165
}
146166
if (paths.length === 1) {
147-
return `${verb} ${basename(first)}...`;
167+
return `${verb} \`${basename(first)}\`...`;
148168
}
149169
if (paths.length === 2 && second) {
150-
return `${verb} ${basename(first)}, ${basename(second)}...`;
170+
return `${verb} \`${basename(first)}\`, \`${basename(second)}\`...`;
151171
}
152-
return `${verb} ${paths.length} files (${basename(first)}${second ? `, ${basename(second)}` : ""}, ...)...`;
172+
return `${verb} ${paths.length} files (\`${basename(first)}\`${second ? `, \`${basename(second)}\`` : ""}, ...)...`;
153173
}
154174

155175
/** Map a patch action to a user-facing verb. */
@@ -188,13 +208,13 @@ async function handleSuspendedStep(
188208
const label = STEP_LABELS[stepId] ?? stepId;
189209

190210
if (payload.type === "local-op") {
191-
const message = describeLocalOp(payload);
211+
const message = renderInlineMarkdown(describeLocalOp(payload));
192212
spin.message(truncateForTerminal(message));
193213

194214
const localResult = await handleLocalOp(payload, options);
195215

196216
if (localResult.message) {
197-
spin.stop(localResult.message);
217+
spin.stop(renderInlineMarkdown(localResult.message));
198218
spin.start("Processing...");
199219
}
200220

test/lib/init/wizard-runner.test.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import * as auth from "../../../src/lib/db/auth.js";
2828
// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
2929
import * as userDb from "../../../src/lib/db/user.js";
3030
import { WizardError } from "../../../src/lib/errors.js";
31+
import { stripAnsi } from "../../../src/lib/formatters/plain-detect.js";
3132
// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
3233
import * as fmt from "../../../src/lib/init/formatters.js";
3334
// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
@@ -628,8 +629,8 @@ describe("runWizard", { timeout: TEST_TIMEOUT_MS }, () => {
628629
) as string[] | undefined;
629630
expect(call).toBeDefined();
630631
const msg = call?.[0] ?? "";
631-
expect(msg.length).toBeLessThanOrEqual(36);
632-
expect(msg.endsWith("…")).toBe(true);
632+
expect(stripAnsi(msg).length).toBeLessThanOrEqual(36);
633+
expect(stripAnsi(msg).endsWith("…")).toBe(true);
633634
} finally {
634635
Object.defineProperty(process.stdout, "columns", {
635636
value: origColumns,
@@ -939,7 +940,7 @@ describe("describeLocalOp", () => {
939940
params: { paths: ["src/settings.py"] },
940941
})
941942
);
942-
expect(msg).toBe("Reading settings.py...");
943+
expect(msg).toBe("Reading `settings.py`...");
943944
});
944945

945946
test("two files shows both basenames", () => {
@@ -949,7 +950,7 @@ describe("describeLocalOp", () => {
949950
params: { paths: ["src/settings.py", "src/urls.py"] },
950951
})
951952
);
952-
expect(msg).toBe("Reading settings.py, urls.py...");
953+
expect(msg).toBe("Reading `settings.py`, `urls.py`...");
953954
});
954955

955956
test("three+ files shows count and first two basenames", () => {
@@ -961,7 +962,7 @@ describe("describeLocalOp", () => {
961962
},
962963
})
963964
);
964-
expect(msg).toBe("Reading 4 files (one.py, two.py, ...)...");
965+
expect(msg).toBe("Reading 4 files (`one.py`, `two.py`, ...)...");
965966
});
966967

967968
test("empty paths array", () => {
@@ -980,7 +981,7 @@ describe("describeLocalOp", () => {
980981
params: { paths: ["requirements.txt"] },
981982
})
982983
);
983-
expect(msg).toBe("Checking requirements.txt...");
984+
expect(msg).toBe("Checking `requirements.txt`...");
984985
});
985986

986987
test("multiple files shows count", () => {
@@ -990,7 +991,7 @@ describe("describeLocalOp", () => {
990991
params: { paths: ["a.py", "b.py", "c.py"] },
991992
})
992993
);
993-
expect(msg).toBe("Checking 3 files (a.py, b.py, ...)...");
994+
expect(msg).toBe("Checking 3 files (`a.py`, `b.py`, ...)...");
994995
});
995996
});
996997

@@ -1004,7 +1005,7 @@ describe("describeLocalOp", () => {
10041005
},
10051006
})
10061007
);
1007-
expect(msg).toBe("Creating sentry.py...");
1008+
expect(msg).toBe("Creating `sentry.py`...");
10081009
});
10091010

10101011
test("single modify shows verb and basename", () => {
@@ -1016,7 +1017,7 @@ describe("describeLocalOp", () => {
10161017
},
10171018
})
10181019
);
1019-
expect(msg).toBe("Modifying settings.py...");
1020+
expect(msg).toBe("Modifying `settings.py`...");
10201021
});
10211022

10221023
test("single delete shows verb and basename", () => {
@@ -1028,7 +1029,7 @@ describe("describeLocalOp", () => {
10281029
},
10291030
})
10301031
);
1031-
expect(msg).toBe("Deleting old.js...");
1032+
expect(msg).toBe("Deleting `old.js`...");
10321033
});
10331034

10341035
test("multiple patches shows count and breakdown", () => {
@@ -1056,7 +1057,7 @@ describe("describeLocalOp", () => {
10561057
params: { commands: ["pip install sentry-sdk"] },
10571058
})
10581059
);
1059-
expect(msg).toBe("Running pip install sentry-sdk...");
1060+
expect(msg).toBe("Running `pip install sentry-sdk`...");
10601061
});
10611062

10621063
test("multiple commands shows count and first", () => {
@@ -1068,7 +1069,7 @@ describe("describeLocalOp", () => {
10681069
},
10691070
})
10701071
);
1071-
expect(msg).toBe("Running 2 commands (pip install sentry-sdk, ...)...");
1072+
expect(msg).toBe("Running 2 commands (`pip install sentry-sdk`, ...)...");
10721073
});
10731074
});
10741075

@@ -1089,7 +1090,7 @@ describe("describeLocalOp", () => {
10891090
params: { name: "my-app", platform: "python-django" },
10901091
})
10911092
);
1092-
expect(msg).toBe('Creating project "my-app" (python-django)...');
1093+
expect(msg).toBe("Creating project `my-app` (python-django)...");
10931094
});
10941095
});
10951096
});

0 commit comments

Comments
 (0)