fix(FN-000): address onboarding review feedback

This commit is contained in:
gsxdsm
2026-06-22 09:13:58 -07:00
parent 2bfac78b47
commit 3c49f591b7
6 changed files with 92 additions and 4 deletions

View File

@@ -1807,6 +1807,11 @@ export function ModelOnboardingModal({
const hasAiProvider = connectedAiProviders.length > 0;
const hasProjectSelected = Boolean(projectId);
const selectedAgentPreset = selectedAgentPresetId ? getPresetById(selectedAgentPresetId) : undefined;
/*
FNXC:Onboarding 2026-06-22-06:03:
AI-generated agent drafts are custom and should not appear selected as a template, but the template radiogroup still needs one tabbable item for keyboard users.
*/
const agentPresetTabStopId = selectedAgentPresetId || ceoPreset.id;
const isAgentActionDisabled = isCreatingAgent || !hasProjectSelected;
// True when on GitHub step but skipped AI setup (no AI provider connected)
const aiSetupSkipped = step === "github" && !hasAiProvider;
@@ -2832,7 +2837,7 @@ export function ModelOnboardingModal({
role="radio"
aria-checked={selected}
aria-label={selected ? t("setup.selectedAgentTemplate", "{{name}} selected", { name: preset.name }) : preset.name}
tabIndex={selected ? 0 : -1}
tabIndex={preset.id === agentPresetTabStopId ? 0 : -1}
data-model-onboarding-agent-preset-id={preset.id}
disabled={isAgentActionDisabled}
onClick={() => handleAgentPresetSelect(preset.id)}
@@ -3185,7 +3190,12 @@ export function ModelOnboardingModal({
</div>
)}
>
<Suspense fallback={null}>
<Suspense fallback={(
<div className="wizard-error setup-wizard-agent-interview-error" role="status">
{t("setup.firstAgentInterviewLoading", "Loading AI Interview...")}
</div>
)}
>
<ExperimentalAgentOnboardingModal
isOpen={isAgentInterviewOpen}
onClose={() => setIsAgentInterviewOpen(false)}

View File

@@ -158,6 +158,10 @@ export function SetupWizardModal({
const result = await registerProject(input);
if (!includeAgentStep) {
setState((prev) => ({
...prev,
isRegistering: false,
}));
onProjectRegistered(result);
return;
}
@@ -267,6 +271,11 @@ export function SetupWizardModal({
? getPresetById(state.selectedPresetId)
: undefined;
const isAgentActionDisabled = state.isCreatingAgent;
/*
FNXC:Onboarding 2026-06-22-06:03:
AI-generated agent drafts are custom and should not appear selected as a template, but the template radiogroup still needs one tabbable item for keyboard users.
*/
const agentPresetTabStopId = state.selectedPresetId || ceoPreset.id;
/*
FNXC:Onboarding 2026-06-22-05:37:
The optional project-agent step needs more horizontal room than project details so templates and preview can be compared side by side.
@@ -510,7 +519,7 @@ export function SetupWizardModal({
role="radio"
aria-checked={selected}
aria-label={selected ? t("setup.selectedAgentTemplate", "{{name}} selected", { name: preset.name }) : preset.name}
tabIndex={selected ? 0 : -1}
tabIndex={preset.id === agentPresetTabStopId ? 0 : -1}
data-agent-preset-id={preset.id}
disabled={isAgentActionDisabled}
onClick={() => handlePresetSelect(preset.id)}
@@ -672,7 +681,12 @@ export function SetupWizardModal({
</div>
)}
>
<Suspense fallback={null}>
<Suspense fallback={(
<div className="wizard-error setup-wizard-agent-interview-error" role="status">
{t("setup.firstAgentInterviewLoading", "Loading AI Interview...")}
</div>
)}
>
<ExperimentalAgentOnboardingModal
isOpen={isInterviewOpen}
onClose={() => setIsInterviewOpen(false)}

View File

@@ -127,6 +127,36 @@ vi.mock("../ProviderIcon", () => ({
),
}));
vi.mock("../ExperimentalAgentOnboardingModal", () => ({
ExperimentalAgentOnboardingModal: ({ isOpen, onClose, onUseDraft }: { isOpen: boolean; onClose: () => void; onUseDraft: (draft: any) => void }) => (
isOpen ? (
<div data-testid="agent-interview-modal">
AI Interview Modal
<button
type="button"
onClick={() => {
onUseDraft({
name: "Launch Coordinator",
title: "Launch Planning Agent",
icon: "◇",
role: "not-a-real-role",
instructionsText: "Coordinate launch tasks.",
soul: "Strategic launch planner.",
skills: ["planning", "review"],
runtimeHint: "codex-local",
maxTurns: 24,
thinkingLevel: "medium",
});
onClose();
}}
>
Use Draft
</button>
</div>
) : null
),
}));
// Mock lucide-react icons - preserve actual icons for other components
vi.mock("lucide-react", async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
@@ -3250,6 +3280,35 @@ describe("ModelOnboardingModal", () => {
});
});
it("keeps one agent template tabbable after applying an AI draft", async () => {
render(
<ModelOnboardingModal
onComplete={vi.fn()}
addToast={vi.fn()}
projectId="proj_123"
agentOnboardingEnabled
/>,
);
await navigateToProjectSetupStep();
fireEvent.click(screen.getByText("Next →"));
await waitFor(() => {
expect(screen.getByText("Create Your First Agent")).toBeTruthy();
});
fireEvent.click(screen.getByText("AI Interview"));
expect(await screen.findByTestId("agent-interview-modal")).toBeTruthy();
fireEvent.click(screen.getByText("Use Draft"));
expect(await screen.findByText("Launch Coordinator")).toBeTruthy();
expect(screen.getByText("Launch Planning Agent")).toBeTruthy();
const ceoRadio = screen.getByRole("radio", { name: "CEO" });
expect(ceoRadio).toHaveAttribute("tabIndex", "0");
expect(ceoRadio).toHaveAttribute("aria-checked", "false");
});
it("allows completing full onboarding flow without any setup", async () => {
// All providers not authenticated, no model selected
mockFetchAuthStatus.mockResolvedValueOnce({

View File

@@ -636,6 +636,9 @@ describe("SetupWizardModal", () => {
expect(await screen.findByText("Launch Coordinator")).toBeDefined();
expect(screen.getByText("Launch Planning Agent")).toBeDefined();
const ceoRadio = screen.getByRole("radio", { name: "CEO" });
expect(ceoRadio).toHaveAttribute("tabIndex", "0");
expect(ceoRadio).toHaveAttribute("aria-checked", "false");
expect(mockCreateAgent).not.toHaveBeenCalled();
fireEvent.click(screen.getByText("Create Agent"));

View File

@@ -6598,6 +6598,7 @@
"firstAgentDraftName": "Draft agent",
"firstAgentContinueWithTemplates": "Continue with templates",
"firstAgentInterviewLoadError": "AI interview could not load. You can still create an agent from a template or skip this step.",
"firstAgentInterviewLoading": "Loading AI Interview...",
"firstAgentIntro": "Agents are optional. Fusion can build tasks without one by starting temporary agents for planning, coding, review, and merge. Create an agent only if you want help coordinating tasks and direction.",
"firstAgentNoInstructions": "No inline instructions yet",
"firstAgentPreview": "Preview",

View File

@@ -6602,6 +6602,7 @@ export default interface Resources {
"firstAgentCustomDraft": "Custom agent draft",
"firstAgentDraftName": "Draft agent",
"firstAgentInterviewLoadError": "AI interview could not load. You can still create an agent from a template or skip this step.",
"firstAgentInterviewLoading": "Loading AI Interview...",
"firstAgentIntro": "Agents are optional. Fusion can build tasks without one by starting temporary agents for planning, coding, review, and merge. Create an agent only if you want help coordinating tasks and direction.",
"firstAgentNoInstructions": "No inline instructions yet",
"firstAgentPreview": "Preview",