Skip to content
23 changes: 7 additions & 16 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ const { handlePoliciesState }: typeof import("./onboard/machine/handlers/policie
const { handlePreflightState }: typeof import("./onboard/machine/handlers/preflight") = require("./onboard/machine/handlers/preflight");
const { handleProviderInferenceState }: typeof import("./onboard/machine/handlers/provider-inference") = require("./onboard/machine/handlers/provider-inference");
const { handleSandboxState }: typeof import("./onboard/machine/handlers/sandbox") = require("./onboard/machine/handlers/sandbox");
const { getOnboardProgressStep }: typeof import("./onboard/machine/progress") = require("./onboard/machine/progress");
const policies: typeof import("./policy") = require("./policy");
const tiers: typeof import("./policy/tiers") = require("./policy/tiers");
const { ensureUsageNoticeConsent } = require("./onboard/usage-notice");
Expand Down Expand Up @@ -6334,28 +6335,18 @@ const recordRepairEvent = onboardRuntimeBoundary.recordRepairEvent.bind(onboardR
const recordPostVerifyStarted = onboardRuntimeBoundary.recordPostVerifyStarted.bind(onboardRuntimeBoundary);
const recordSessionComplete = onboardRuntimeBoundary.recordSessionComplete.bind(onboardRuntimeBoundary);

const ONBOARD_STEP_INDEX: Record<string, { number: number; title: string }> = {
preflight: { number: 1, title: "Preflight checks" },
gateway: { number: 2, title: "Starting OpenShell gateway" },
provider_selection: { number: 3, title: "Configuring inference (NIM)" },
inference: { number: 4, title: "Setting up inference provider" },
messaging: { number: 5, title: "Messaging channels" },
sandbox: { number: 6, title: "Creating sandbox" },
openclaw: { number: 7, title: "Setting up agent inside sandbox" },
policies: { number: 8, title: "Policy presets" },
};

function skippedStepMessage(
stepName: string,
detail?: string | null,
reason: "resume" | "reuse" = "resume",
): void {
let stepInfo = ONBOARD_STEP_INDEX[stepName];
if (stepInfo && stepName === "openclaw") {
stepInfo = { ...stepInfo, title: `Setting up ${agentProductName()} inside sandbox` };
}
const progressStep = getOnboardProgressStep(stepName);
const stepInfo =
progressStep && stepName === "openclaw"
? { ...progressStep, title: `Setting up ${agentProductName()} inside sandbox` }
: progressStep;
if (stepInfo) {
step(stepInfo.number, 8, stepInfo.title);
step(stepInfo.number, stepInfo.total, stepInfo.title);
}
const prefix = reason === "reuse" ? "[reuse]" : "[resume]";
console.log(` ${prefix} Skipping ${stepName}${detail ? ` (${detail})` : ""}`);
Expand Down
1 change: 0 additions & 1 deletion src/lib/onboard/machine/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export const ONBOARD_MACHINE_STATE_DEFINITIONS = [
state: "agent_setup",
terminal: false,
stepName: "agent_setup",
progress: { number: 7, total: 8, title: "Setting up agent inside sandbox" },
},
{
state: "openclaw",
Expand Down
38 changes: 38 additions & 0 deletions src/lib/onboard/machine/progress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import { ONBOARD_MACHINE_STATE_DEFINITIONS } from "./definition";
import { getOnboardProgressStep, ONBOARD_PROGRESS_STEPS } from "./progress";

describe("onboard progress metadata", () => {
it("derives state-backed progress labels from machine definitions", () => {
for (const definition of ONBOARD_MACHINE_STATE_DEFINITIONS) {
if (!("progress" in definition)) continue;
expect(ONBOARD_PROGRESS_STEPS[definition.stepName]).toEqual(definition.progress);
}
});

it("preserves the existing eight-step onboarding labels", () => {
expect(ONBOARD_PROGRESS_STEPS).toEqual({
preflight: { number: 1, total: 8, title: "Preflight checks" },
gateway: { number: 2, total: 8, title: "Starting OpenShell gateway" },
provider_selection: { number: 3, total: 8, title: "Configuring inference (NIM)" },
inference: { number: 4, total: 8, title: "Setting up inference provider" },
messaging: { number: 5, total: 8, title: "Messaging channels" },
sandbox: { number: 6, total: 8, title: "Creating sandbox" },
openclaw: { number: 7, total: 8, title: "Setting up agent inside sandbox" },
policies: { number: 8, total: 8, title: "Policy presets" },
});
});

it("looks up known labels and ignores unknown steps", () => {
expect(getOnboardProgressStep("gateway")).toEqual({
number: 2,
total: 8,
title: "Starting OpenShell gateway",
});
expect(getOnboardProgressStep("not-a-step")).toBeNull();
});
});
42 changes: 42 additions & 0 deletions src/lib/onboard/machine/progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
ONBOARD_MACHINE_STATE_DEFINITIONS,
type OnboardMachineStateWithProgressDefinition,
} from "./definition";

export interface OnboardProgressStep {
number: number;
total: number;
title: string;
}

export type OnboardMachineProgressStepName =
OnboardMachineStateWithProgressDefinition["stepName"];

export type OnboardProgressStepName = OnboardMachineProgressStepName | "messaging";

// Messaging is still emitted inside the sandbox flow rather than represented as
// a session/FSM state. Keep this legacy pseudo-step here only while the progress
// API preserves that visible label; remove it when messaging becomes a real
// FSM-backed onboarding step or the legacy pseudo-step lookup goes away.
const EXTRA_PROGRESS_STEPS = [
{
stepName: "messaging",
progress: { number: 5, total: 8, title: "Messaging channels" },
},
] as const;

export const ONBOARD_PROGRESS_STEPS = Object.fromEntries([
...ONBOARD_MACHINE_STATE_DEFINITIONS.flatMap((definition) =>
"progress" in definition ? [[definition.stepName, definition.progress]] : [],
),
...EXTRA_PROGRESS_STEPS.map((definition) => [definition.stepName, definition.progress]),
]) as Readonly<Record<OnboardProgressStepName, OnboardProgressStep>>;

export function getOnboardProgressStep(stepName: string): OnboardProgressStep | null {
return Object.prototype.hasOwnProperty.call(ONBOARD_PROGRESS_STEPS, stepName)
? ONBOARD_PROGRESS_STEPS[stepName as OnboardProgressStepName]
: null;
}