Skip to content

Commit 7b6a33c

Browse files
authored
fix(init): keep Ink task progress in sync (#1259)
## Summary The Ink sidebar was only updating tasks when a workflow step suspended. Steps that completed entirely inside start/resume, such as Analyze project when its inputs were already cached, were therefore shown as skipped or left incomplete even though Mastra had finished them. This reconciles the checklist from the step statuses Mastra already returns on every response. Tasks that never suspend now become green, failures are reflected even before a client-tool round trip, and the final synchronous steps can reach the completed count. The separate Installing dependencies row was also stale: cli-init-api#140 merged installation into apply-codemods. The checklist now mirrors the real eight-step user-facing workflow and labels that phase Applying changes + deps instead of keeping a task that can never receive a server transition. Tasks now render above the rotating tips, so different tip heights no longer move the progress list around. ## Test plan - pnpm run lint — 920 files pass - pnpm run typecheck — passes - pnpm vitest run test/lib/init/ui/ink-app.snapshot.test.tsx test/lib/init/ui/wizard-store.test.ts test/lib/init/wizard-runner.test.ts — 123 tests pass - pnpm vitest run test/lib/init test/lib/telemetry.test.ts — 538 tests pass - TZ=UTC pnpm vitest run test/lib/time-range.test.ts — 51 tests pass - pnpm run test:unit — 8,479 pass, 14 skipped, with eight pre-existing local-environment failures: six timezone-sensitive time-range assertions under Europe/Madrid and two bash completion shell simulations. The time-range failures pass under TZ=UTC; none of the eight touch init. - git diff --check — passes - Local patch coverage for executable lines — 100%
1 parent df6803b commit 7b6a33c

7 files changed

Lines changed: 188 additions & 49 deletions

File tree

src/lib/init/clack-utils.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,8 @@ export const STEP_LABELS: Record<string, string> = {
117117
"detect-platform": "Detecting platform and framework",
118118
"ensure-sentry-project": "Setting up Sentry project",
119119
"select-features": "Selecting features",
120-
"install-deps": "Installing dependencies",
121120
"plan-codemods": "Planning code modifications",
122-
"apply-codemods": "Applying code modifications",
121+
"apply-codemods": "Applying code modifications and installing dependencies",
123122
"verify-changes": "Verifying changes",
124123
"open-sentry-ui": "Finishing up",
125124
};
@@ -147,7 +146,6 @@ export const CANONICAL_STEP_ORDER: readonly string[] = [
147146
"select-features",
148147
"plan-codemods",
149148
"apply-codemods",
150-
"install-deps",
151149
"verify-changes",
152150
"open-sentry-ui",
153151
];
@@ -156,13 +154,15 @@ export const CANONICAL_STEP_ORDER: readonly string[] = [
156154
* Subset of {@link CANONICAL_STEP_ORDER} surfaced in the progress
157155
* checklist. The Ink sidebar is 36 cols wide and shares vertical
158156
* space with the tip card and the files-read panel, so showing all
159-
* 12 step rows would push the files panel off-screen on shorter
157+
* 11 step rows would push the files panel off-screen on shorter
160158
* terminals.
161159
*
162160
* The hidden steps (`select-target-app`, `resolve-dir`,
163161
* `check-existing-sentry`) are plumbing — users care that "Setting up
164162
* Sentry project" happened, not that we resolved their working
165-
* directory along the way.
163+
* directory along the way. Dependency installation is part of
164+
* `apply-codemods`; the server removed the separate `install-deps` step in
165+
* getsentry/cli-init-api#140.
166166
*/
167167
export const CHECKLIST_VISIBLE_STEPS: readonly string[] = [
168168
"discover-context",
@@ -171,7 +171,6 @@ export const CHECKLIST_VISIBLE_STEPS: readonly string[] = [
171171
"select-features",
172172
"plan-codemods",
173173
"apply-codemods",
174-
"install-deps",
175174
"verify-changes",
176175
"open-sentry-ui",
177176
];
@@ -188,9 +187,8 @@ export const STEP_ACTIVE_LABELS: Record<string, string> = {
188187
"detect-platform": "Detecting framework and platform...",
189188
"ensure-sentry-project": "Configuring Sentry project...",
190189
"select-features": "Preparing feature selection...",
191-
"install-deps": "Installing Sentry SDK and dependencies...",
192190
"plan-codemods": "Planning code changes...",
193-
"apply-codemods": "Applying code modifications...",
191+
"apply-codemods": "Applying code changes and installing dependencies...",
194192
"verify-changes": "Verifying setup...",
195193
"open-sentry-ui": "Finishing up...",
196194
};
@@ -207,9 +205,8 @@ export const STEP_LABELS_SHORT: Record<string, string> = {
207205
"detect-platform": "Detecting platform",
208206
"ensure-sentry-project": "Setting up project",
209207
"select-features": "Selecting features",
210-
"install-deps": "Installing deps",
211208
"plan-codemods": "Planning changes",
212-
"apply-codemods": "Applying changes",
209+
"apply-codemods": "Applying changes + deps",
213210
"verify-changes": "Verifying changes",
214211
"open-sentry-ui": "Finishing up",
215212
};

src/lib/init/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,19 @@ export type WorkflowRunResult = {
273273
status: "suspended" | "success" | "failed";
274274
suspended?: string[][];
275275
activeStepsPath?: Record<string, unknown>;
276-
steps?: Record<string, { suspendPayload?: unknown }>;
276+
steps?: Record<
277+
string,
278+
{
279+
status?:
280+
| "success"
281+
| "failed"
282+
| "suspended"
283+
| "running"
284+
| "waiting"
285+
| "paused";
286+
suspendPayload?: unknown;
287+
}
288+
>;
277289
suspendPayload?: unknown;
278290
result?: WizardOutput;
279291
error?: string;

src/lib/init/ui/ink-app.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
*
77
* ┌─ ◆ Sentry Init Wizard ──────────────────── sentry.io ─┐
88
* │ │
9-
* │ ╔═══╗ │ ╭ Did you know? ─────────╮ │
10-
* │ ║ S ║ Sentry banner │ │ <tip> │ │
11-
* │ ╚═══╝ │ ╰────────────────────────╯
12-
* │ ● log line │
13-
* │ ▲ log line │ ╭ Tasks ────── 2/9 ──────╮
14-
* │ ◐ spinner... │ │ ◼ Discover ctx
15-
* │ [PromptArea] │ │ ▶ Install deps │ │
16-
* │ │ │ ◻ Apply codemods │ │
9+
* │ ╔═══╗ │ ╭ Tasks ────── 2/8 ──────╮ │
10+
* │ ║ S ║ Sentry banner │ │ ◼ Analyze project │ │
11+
* │ ╚═══╝ │ │ ▶ Apply changes │
12+
* │ ● log line │ ╰────────────────────────╯
13+
* │ ▲ log line │
14+
* │ │ ╭ Did you know? ─────────╮
15+
* │ ◐ spinner... │ │ Errors → context → fix │ │
16+
* │ [PromptArea] │ │ <tip> │ │
1717
* │ │ ╰────────────────────────╯ │
1818
* │ ● Status Files │
1919
* │ ←→ switch tab │
@@ -340,17 +340,17 @@ function Sidebar({
340340
const showTips = terminalRows >= 24;
341341
return (
342342
<Box flexDirection="column" overflow="hidden" width="40%">
343+
<ProgressPanel steps={steps} />
343344
{showTips ? (
344345
<>
346+
<Box height={1} />
345347
{learnState.complete ? (
346348
<TipPanel tipIndex={tipIndex} />
347349
) : (
348350
<LearnPanel learnState={learnState} />
349351
)}
350-
<Box height={1} />
351352
</>
352353
) : null}
353-
<ProgressPanel steps={steps} />
354354
</Box>
355355
);
356356
}

src/lib/init/wizard-runner.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,8 @@ export async function runWizard(initialOptions: WizardOptions): Promise<void> {
980980
const stepPhases = new Map<string, number>();
981981
const stepHistory = new Map<string, CompactPhaseHistoryEntry[]>();
982982

983+
syncWorkflowStepStatuses(result, ui);
984+
983985
// Track which step the runner is currently suspended on so the
984986
// sidebar checklist can flip rows as the workflow advances. A
985987
// single step can suspend multiple times (read-files → analyze →
@@ -1054,6 +1056,7 @@ export async function runWizard(initialOptions: WizardOptions): Promise<void> {
10541056
ui,
10551057
progressRotation,
10561058
});
1059+
syncWorkflowStepStatuses(result, ui);
10571060
} finally {
10581061
progressRotation.stop();
10591062
}
@@ -1128,6 +1131,27 @@ export async function runWizard(initialOptions: WizardOptions): Promise<void> {
11281131
}
11291132
}
11301133

1134+
/**
1135+
* Reconcile checklist rows with Mastra's persisted step results.
1136+
*
1137+
* A step can finish entirely inside a start/resume request without ever
1138+
* suspending for CLI work. Those steps still appear in `result.steps`, so
1139+
* their server-reported status is more complete than the suspend transitions
1140+
* observed by the client.
1141+
*/
1142+
function syncWorkflowStepStatuses(
1143+
result: WorkflowRunResult,
1144+
ui: WizardUI
1145+
): void {
1146+
for (const [stepId, step] of Object.entries(result.steps ?? {})) {
1147+
if (step.status === "success") {
1148+
ui.setStep?.(stepId, "completed");
1149+
} else if (step.status === "failed") {
1150+
ui.setStep?.(stepId, "failed");
1151+
}
1152+
}
1153+
}
1154+
11311155
// biome-ignore lint/nursery/useMaxParams: existing 4-param shape; cwd is a defaulted extension
11321156
export async function handleFinalResult(
11331157
result: WorkflowRunResult,

test/lib/init/ui/ink-app.snapshot.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@ describe("Ink App snapshot", () => {
222222
expect(frame).toContain("2/7");
223223
});
224224

225+
test("keeps tasks above rotating tips", async () => {
226+
const store = new WizardStore({
227+
learnState: { blockIndex: 0, lineIndex: 0, complete: true },
228+
});
229+
230+
const frame = stripAnsi((await renderApp(store, 120)).allOutput());
231+
expect(frame).toContain("Did you know?");
232+
expect(frame.indexOf("Tasks")).toBeLessThan(frame.indexOf("Did you know?"));
233+
});
234+
225235
test("renders single-column layout at narrow width", async () => {
226236
const store = new WizardStore();
227237
store.appendLog("info", "Narrow terminal");

test/lib/init/ui/wizard-store.test.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ describe("WizardStore step progress", () => {
3030
expect(snapshot.steps.every((entry) => entry.status === "pending")).toBe(
3131
true
3232
);
33+
expect(snapshot.steps.some((entry) => entry.id === "install-deps")).toBe(
34+
false
35+
);
36+
expect(
37+
snapshot.steps.find((entry) => entry.id === "apply-codemods")?.label
38+
).toBe("Applying changes + deps");
3339
});
3440

3541
test("flips a step to in_progress on first call", () => {
@@ -43,9 +49,9 @@ describe("WizardStore step progress", () => {
4349

4450
test("re-entering an in_progress step is idempotent (no flicker)", () => {
4551
const store = new WizardStore();
46-
store.setStepStatus("install-deps", "in_progress");
52+
store.setStepStatus("apply-codemods", "in_progress");
4753
const before = store.getSnapshot().steps;
48-
store.setStepStatus("install-deps", "in_progress");
54+
store.setStepStatus("apply-codemods", "in_progress");
4955
const after = store.getSnapshot().steps;
5056
// Reference equality: no update emitted, so the array is the
5157
// same instance. This is what the store guarantees for no-op
@@ -57,16 +63,16 @@ describe("WizardStore step progress", () => {
5763
const store = new WizardStore();
5864
// Jump straight to a later step — simulates the workflow
5965
// taking an `if`-branch that bypassed the earlier ones.
60-
store.setStepStatus("install-deps", "in_progress");
66+
store.setStepStatus("verify-changes", "in_progress");
6167
const steps = store.getSnapshot().steps;
62-
const installIdx = CANONICAL_STEP_ORDER.indexOf("install-deps");
68+
const verifyIndex = CANONICAL_STEP_ORDER.indexOf("verify-changes");
6369
for (const entry of steps) {
6470
const idx = CANONICAL_STEP_ORDER.indexOf(entry.id);
65-
if (idx >= 0 && idx < installIdx) {
71+
if (idx >= 0 && idx < verifyIndex) {
6672
expect(entry.status).toBe("skipped");
6773
}
6874
}
69-
expect(steps.find((entry) => entry.id === "install-deps")?.status).toBe(
75+
expect(steps.find((entry) => entry.id === "verify-changes")?.status).toBe(
7076
"in_progress"
7177
);
7278
});
@@ -75,7 +81,7 @@ describe("WizardStore step progress", () => {
7581
const store = new WizardStore();
7682
store.setStepStatus("discover-context", "in_progress");
7783
store.setStepStatus("discover-context", "completed");
78-
store.setStepStatus("install-deps", "in_progress");
84+
store.setStepStatus("verify-changes", "in_progress");
7985
const discover = store
8086
.getSnapshot()
8187
.steps.find((row) => row.id === "discover-context");
@@ -113,11 +119,11 @@ describe("WizardStore step progress", () => {
113119

114120
test("failed transition wins over the existing status", () => {
115121
const store = new WizardStore();
116-
store.setStepStatus("install-deps", "in_progress");
117-
store.setStepStatus("install-deps", "failed");
122+
store.setStepStatus("apply-codemods", "in_progress");
123+
store.setStepStatus("apply-codemods", "failed");
118124
const entry = store
119125
.getSnapshot()
120-
.steps.find((row) => row.id === "install-deps");
126+
.steps.find((row) => row.id === "apply-codemods");
121127
expect(entry?.status).toBe("failed");
122128
});
123129

@@ -139,9 +145,9 @@ describe("WizardStore step progress", () => {
139145
const unsubscribe = store.subscribe(() => {
140146
notifications += 1;
141147
});
142-
store.setStepStatus("install-deps", "in_progress");
143-
store.setStepStatus("install-deps", "in_progress"); // idempotent
144-
store.setStepStatus("install-deps", "completed");
148+
store.setStepStatus("apply-codemods", "in_progress");
149+
store.setStepStatus("apply-codemods", "in_progress"); // idempotent
150+
store.setStepStatus("apply-codemods", "completed");
145151
unsubscribe();
146152
// Two real transitions = two notifications. The middle no-op
147153
// does not fire a listener — saves a render in React.

0 commit comments

Comments
 (0)