diff --git a/Assets/Tests/Editor/SetupWizardWindowTests.cs b/Assets/Tests/Editor/SetupWizardWindowTests.cs index 00564439f7..8baa5225f8 100644 --- a/Assets/Tests/Editor/SetupWizardWindowTests.cs +++ b/Assets/Tests/Editor/SetupWizardWindowTests.cs @@ -455,7 +455,7 @@ public void FilterInstallableSkillTargets_ExcludesTargetsWithoutSkillsDirectory( }; List installableTargets = - SetupWizardWindow.FilterInstallableSkillTargets(targets); + SetupWizardSkillsStepPresenter.FilterInstallableSkillTargets(targets); Assert.That(installableTargets.Count, Is.EqualTo(2)); Assert.That(installableTargets[0].DirName, Is.EqualTo(".claude")); @@ -498,7 +498,7 @@ public void CanManageSkills_WhenCliIsInstalled_ReturnsTrue() public void ShouldShowSkillsTargetRow_WhenFirstInstallAndCliMissing_ReturnsTrue() { // Verifies that first-time setup can choose a skill target before CLI installation. - bool shouldShow = SetupWizardWindow.ShouldShowSkillsTargetRowForSetupWizard( + bool shouldShow = SetupWizardSkillsStepPresenter.ShouldShowSkillsTargetRowForSetupWizard( shouldUseFirstInstallSkillsUi: true); Assert.That(shouldShow, Is.True); @@ -508,7 +508,7 @@ public void ShouldShowSkillsTargetRow_WhenFirstInstallAndCliMissing_ReturnsTrue( public void ShouldShowSkillsTargetRow_WhenNotFirstInstall_ReturnsFalse() { // Verifies that returning setup keeps the compact target row hidden. - bool shouldShow = SetupWizardWindow.ShouldShowSkillsTargetRowForSetupWizard( + bool shouldShow = SetupWizardSkillsStepPresenter.ShouldShowSkillsTargetRowForSetupWizard( shouldUseFirstInstallSkillsUi: false); Assert.That(shouldShow, Is.False); @@ -518,7 +518,7 @@ public void ShouldShowSkillsTargetRow_WhenNotFirstInstall_ReturnsFalse() public void ShouldShowSkillsTargetList_WhenCliMissing_ReturnsFalse() { // Verifies that multi-target status rows stay hidden until the CLI can inspect skill state. - bool shouldShow = SetupWizardWindow.ShouldShowSkillsTargetListForSetupWizard( + bool shouldShow = SetupWizardSkillsStepPresenter.ShouldShowSkillsTargetListForSetupWizard( canManageSkills: false, shouldUseFirstInstallSkillsUi: false); @@ -529,7 +529,7 @@ public void ShouldShowSkillsTargetList_WhenCliMissing_ReturnsFalse() public void ShouldShowSkillsTargetList_WhenCliInstalledAndNotFirstInstall_ReturnsTrue() { // Verifies that returning users keep the multi-target skill status view. - bool shouldShow = SetupWizardWindow.ShouldShowSkillsTargetListForSetupWizard( + bool shouldShow = SetupWizardSkillsStepPresenter.ShouldShowSkillsTargetListForSetupWizard( canManageSkills: true, shouldUseFirstInstallSkillsUi: false); @@ -649,7 +649,7 @@ public void ShouldCheckCliPathSetupForSetupWizard_RequiresPackageOwnedCli( public void CreateFirstInstallSkillTarget_WhenClaudeSelected_ReturnsClaudeProjectTarget() { SkillSetupTargetInfo target = - SetupWizardWindow.CreateFirstInstallSkillTarget(SkillsTarget.Claude, true); + SetupWizardSkillsStepPresenter.CreateFirstInstallSkillTarget(SkillsTarget.Claude, true); Assert.That(target.DisplayName, Is.EqualTo("Claude Code")); Assert.That(target.DirName, Is.EqualTo(".claude")); @@ -669,7 +669,7 @@ public void CreateFirstInstallSkillTarget_ReturnsMappedTarget( string expectedInstallFlag) { SkillSetupTargetInfo target = - SetupWizardWindow.CreateFirstInstallSkillTarget(targetType, true); + SetupWizardSkillsStepPresenter.CreateFirstInstallSkillTarget(targetType, true); Assert.That(target.DisplayName, Is.EqualTo(expectedDisplayName)); Assert.That(target.DirName, Is.EqualTo(expectedDirName)); @@ -682,7 +682,7 @@ public void CreateFirstInstallSkillTarget_ReturnsMappedTarget( public void CreateFirstInstallSkillTarget_WhenGroupingDisabled_KeepsTargetMetadata() { SkillSetupTargetInfo target = - SetupWizardWindow.CreateFirstInstallSkillTarget(SkillsTarget.Claude, false); + SetupWizardSkillsStepPresenter.CreateFirstInstallSkillTarget(SkillsTarget.Claude, false); Assert.That(target.DisplayName, Is.EqualTo("Claude Code")); Assert.That(target.DirName, Is.EqualTo(".claude")); @@ -703,7 +703,7 @@ public void GetSelectedSkillTargetInfo_WhenDetectedTargetExists_ReturnsDetectedS installState: SkillInstallState.Installed) }; - SkillSetupTargetInfo target = SetupWizardWindow.GetSelectedSkillTargetInfo( + SkillSetupTargetInfo target = SetupWizardSkillsStepPresenter.GetSelectedSkillTargetInfo( targets, SkillsTarget.Claude, groupSkillsUnderUnityCliLoop: true); @@ -727,7 +727,7 @@ public void GetFirstInstallableSkillTargets_WhenSelectedTargetIsInstalled_Return }; List installableTargets = - SetupWizardWindow.GetFirstInstallableSkillTargets( + SetupWizardSkillsStepPresenter.GetFirstInstallableSkillTargets( targets, SkillsTarget.Claude, groupSkillsUnderUnityCliLoop: true); @@ -739,7 +739,7 @@ public void GetFirstInstallableSkillTargets_WhenSelectedTargetIsInstalled_Return public void GetFirstInstallableSkillTargets_WhenSelectedTargetIsMissing_ReturnsMappedTarget() { List installableTargets = - SetupWizardWindow.GetFirstInstallableSkillTargets( + SetupWizardSkillsStepPresenter.GetFirstInstallableSkillTargets( new List(), SkillsTarget.Claude, groupSkillsUnderUnityCliLoop: true); @@ -761,7 +761,7 @@ public void GetSkillInstallStatusText_ReturnsExpectedLabel( bool groupSkillsUnderUnityCliLoop, string expectedLabel) { - string label = SetupWizardWindow.GetSkillInstallStatusText( + string label = SetupWizardSkillsStepPresenter.GetSkillInstallStatusText( installState, hasDifferentLayoutSkills, groupSkillsUnderUnityCliLoop); @@ -777,7 +777,7 @@ public void GetInstallSkillsButtonText_ReturnsExpectedLabel( bool hasOutdatedSkills, string expectedLabel) { - string label = SetupWizardWindow.GetInstallSkillsButtonText( + string label = SetupWizardSkillsStepPresenter.GetInstallSkillsButtonText( isInstallingSkills, hasOutdatedSkills); @@ -794,7 +794,7 @@ public void GetSkillsButtonTextForSetupWizard_ReturnsExpectedLabel( bool hasOutdatedSkills, string expectedLabel) { - string label = SetupWizardWindow.GetSkillsButtonTextForSetupWizard( + string label = SetupWizardSkillsStepPresenter.GetSkillsButtonTextForSetupWizard( cliInstalled, isInstallingSkills, hasOutdatedSkills); @@ -865,7 +865,7 @@ public void GetSkillInstallStatusClass_ReturnsExpectedClass( bool groupSkillsUnderUnityCliLoop, string expectedClass) { - string className = SetupWizardWindow.GetSkillInstallStatusClass( + string className = SetupWizardSkillsStepPresenter.GetSkillInstallStatusClass( installState, hasDifferentLayoutSkills, groupSkillsUnderUnityCliLoop); diff --git a/Packages/src/Editor/Presentation/Setup/SetupWizardSkillsStepPresenter.cs b/Packages/src/Editor/Presentation/Setup/SetupWizardSkillsStepPresenter.cs new file mode 100644 index 0000000000..3216ae6d51 --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/SetupWizardSkillsStepPresenter.cs @@ -0,0 +1,328 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UIElements; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Presentation +{ + /// + /// Presents the setup wizard skill installation step. + /// + internal sealed class SetupWizardSkillsStepPresenter + { + private readonly VisualElement _skillsTargetRow; + private readonly VisualElement _skillsTargetList; + private readonly VisualElement _skillsStatusDivider; + private readonly Label _skillsStatusLabel; + private readonly Button _installSkillsButton; + + internal SetupWizardSkillsStepPresenter( + VisualElement skillsTargetRow, + VisualElement skillsTargetList, + VisualElement skillsStatusDivider, + Label skillsStatusLabel, + Button installSkillsButton, + System.Action onInstallSkillsClicked) + { + Debug.Assert(skillsTargetRow != null, "skillsTargetRow must not be null"); + Debug.Assert(skillsTargetList != null, "skillsTargetList must not be null"); + Debug.Assert(skillsStatusDivider != null, "skillsStatusDivider must not be null"); + Debug.Assert(skillsStatusLabel != null, "skillsStatusLabel must not be null"); + Debug.Assert(installSkillsButton != null, "installSkillsButton must not be null"); + Debug.Assert(onInstallSkillsClicked != null, "onInstallSkillsClicked must not be null"); + + _skillsTargetRow = skillsTargetRow + ?? throw new System.ArgumentNullException(nameof(skillsTargetRow)); + _skillsTargetList = skillsTargetList + ?? throw new System.ArgumentNullException(nameof(skillsTargetList)); + _skillsStatusDivider = skillsStatusDivider + ?? throw new System.ArgumentNullException(nameof(skillsStatusDivider)); + _skillsStatusLabel = skillsStatusLabel + ?? throw new System.ArgumentNullException(nameof(skillsStatusLabel)); + _installSkillsButton = installSkillsButton + ?? throw new System.ArgumentNullException(nameof(installSkillsButton)); + _installSkillsButton.clicked += onInstallSkillsClicked + ?? throw new System.ArgumentNullException(nameof(onInstallSkillsClicked)); + } + + internal void ShowChecking(bool shouldUseFirstInstallSkillsUi) + { + UpdateSkillsStatusLabel("Checking installed skills..."); + _installSkillsButton.SetEnabled(false); + _installSkillsButton.text = "Checking..."; + ViewDataBinder.SetVisible(_skillsTargetRow, shouldUseFirstInstallSkillsUi); + ViewDataBinder.SetVisible(_skillsTargetList, !shouldUseFirstInstallSkillsUi); + _skillsTargetList.Clear(); + } + + internal void Update( + bool canManageSkills, + List targets, + bool shouldUseFirstInstallSkillsUi, + SkillsTarget selectedTarget, + bool groupSkillsUnderUnityCliLoop, + bool isInstallingSkills) + { + _skillsTargetList.Clear(); + ViewDataBinder.SetVisible( + _skillsTargetRow, + ShouldShowSkillsTargetRowForSetupWizard(shouldUseFirstInstallSkillsUi)); + ViewDataBinder.SetVisible( + _skillsTargetList, + ShouldShowSkillsTargetListForSetupWizard(canManageSkills, shouldUseFirstInstallSkillsUi)); + + if (!canManageSkills) + { + UpdateSkillsStatusLabel(string.Empty); + _installSkillsButton.SetEnabled(false); + _installSkillsButton.text = GetSkillsButtonTextForSetupWizard( + cliInstalled: false, + isInstallingSkills, + hasOutdatedSkills: false); + return; + } + + if (shouldUseFirstInstallSkillsUi) + { + SkillSetupTargetInfo selectedTargetInfo = GetSelectedSkillTargetInfo( + targets, + selectedTarget, + groupSkillsUnderUnityCliLoop); + UpdateSkillsStatusLabel(string.Empty); + _installSkillsButton.text = CliSetupSection.GetInstallSkillsButtonText( + isCliInstalled: true, + isInstallingSkills, + selectedTargetInfo.InstallState); + _installSkillsButton.SetEnabled(CliSetupSection.IsInstallSkillsButtonEnabled( + isCliInstalled: true, + isInstallingSkills, + selectedTargetInfo.InstallState)); + return; + } + + List installableTargets = FilterInstallableSkillTargets(targets); + + foreach (SkillSetupTargetInfo target in installableTargets) + { + VisualElement item = new(); + item.AddToClassList("setup-target-item"); + + Label nameLabel = new($"{target.DisplayName} ({target.DirName}/)"); + nameLabel.AddToClassList("setup-target-item__label"); + item.Add(nameLabel); + + Label statusLabel = new(GetSkillInstallStatusText( + target.InstallState, + target.HasDifferentLayoutSkills, + groupSkillsUnderUnityCliLoop)); + statusLabel.AddToClassList("setup-target-item__status"); + statusLabel.AddToClassList(GetSkillInstallStatusClass( + target.InstallState, + target.HasDifferentLayoutSkills, + groupSkillsUnderUnityCliLoop)); + item.Add(statusLabel); + + _skillsTargetList.Add(item); + } + + if (installableTargets.Count == 0) + { + UpdateSkillsStatusLabel( + "Create a tool folder to enable skill installation (.claude/, .agents/, etc.)"); + _installSkillsButton.SetEnabled(false); + _installSkillsButton.text = "Install Skills"; + return; + } + + bool isCheckingSkills = installableTargets.Any( + t => t.InstallState == SkillInstallState.Checking); + if (isCheckingSkills) + { + UpdateSkillsStatusLabel("Checking installed skills..."); + _installSkillsButton.SetEnabled(false); + _installSkillsButton.text = "Checking..."; + return; + } + + bool allSkillsInstalled = installableTargets.All( + t => t.InstallState == SkillInstallState.Installed); + if (allSkillsInstalled) + { + UpdateSkillsStatusLabel($"Installed for {installableTargets.Count} targets"); + _installSkillsButton.SetEnabled(false); + _installSkillsButton.text = "Installed"; + } + else + { + bool hasOutdatedSkills = installableTargets.Any( + t => t.InstallState == SkillInstallState.Outdated); + UpdateSkillsStatusLabel(string.Empty); + _installSkillsButton.SetEnabled(!isInstallingSkills); + _installSkillsButton.text = GetSkillsButtonTextForSetupWizard( + cliInstalled: true, + isInstallingSkills, + hasOutdatedSkills); + } + } + + internal static List FilterInstallableSkillTargets( + IEnumerable targets) + { + Debug.Assert(targets != null, "targets must not be null"); + return targets + .Where(target => target.HasSkillsDirectory) + .ToList(); + } + + internal static bool ShouldShowSkillsTargetRowForSetupWizard(bool shouldUseFirstInstallSkillsUi) + { + return shouldUseFirstInstallSkillsUi; + } + + internal static bool ShouldShowSkillsTargetListForSetupWizard( + bool canManageSkills, + bool shouldUseFirstInstallSkillsUi) + { + return canManageSkills && !shouldUseFirstInstallSkillsUi; + } + + internal static SkillSetupTargetInfo CreateFirstInstallSkillTarget( + SkillsTarget target, + bool groupSkillsUnderUnityCliLoop) + { + SkillsTargetSelection selection = SkillsTargetSelectionResolver.Resolve( + target, + groupSkillsUnderUnityCliLoop); + return new( + selection.DisplayName, + selection.DirectoryName, + selection.InstallFlag, + hasSkillsDirectory: false, + hasExistingSkills: false, + hasDifferentLayoutSkills: false, + SkillInstallState.Missing); + } + + internal static SkillSetupTargetInfo GetSelectedSkillTargetInfo( + IEnumerable targets, + SkillsTarget target, + bool groupSkillsUnderUnityCliLoop) + { + Debug.Assert(targets != null, "targets must not be null"); + + SkillsTargetSelection selection = SkillsTargetSelectionResolver.Resolve( + target, + groupSkillsUnderUnityCliLoop); + SkillSetupTargetInfo selectedTargetInfo = targets + .FirstOrDefault(info => info.DirName == selection.DirectoryName); + return string.IsNullOrEmpty(selectedTargetInfo.DirName) + ? CreateFirstInstallSkillTarget(target, groupSkillsUnderUnityCliLoop) + : selectedTargetInfo; + } + + internal static List GetFirstInstallableSkillTargets( + IEnumerable targets, + SkillsTarget target, + bool groupSkillsUnderUnityCliLoop) + { + SkillSetupTargetInfo selectedTargetInfo = GetSelectedSkillTargetInfo( + targets, + target, + groupSkillsUnderUnityCliLoop); + return selectedTargetInfo.InstallState == SkillInstallState.Installed + || selectedTargetInfo.InstallState == SkillInstallState.Checking + ? new List() + : new List { selectedTargetInfo }; + } + + internal static string GetSkillsButtonTextForSetupWizard( + bool cliInstalled, + bool isInstallingSkills, + bool hasOutdatedSkills) + { + return !cliInstalled + ? "Install Skills" + : GetInstallSkillsButtonText(isInstallingSkills, hasOutdatedSkills); + } + + internal static string GetInstallSkillsButtonText( + bool isInstallingSkills, + bool hasOutdatedSkills) + { + if (isInstallingSkills) + { + return "Installing..."; + } + + return hasOutdatedSkills ? "Update Skills" : "Install Skills"; + } + + internal static string GetSkillInstallStatusText( + SkillInstallState installState, + bool hasDifferentLayoutSkills, + bool groupSkillsUnderUnityCliLoop) + { + if (installState == SkillInstallState.Checking) + { + return "Checking..."; + } + + if (installState == SkillInstallState.Installed) + { + return "Installed"; + } + + if (installState == SkillInstallState.Outdated) + { + return "Outdated"; + } + + if (!hasDifferentLayoutSkills) + { + return "Missing"; + } + + return groupSkillsUnderUnityCliLoop ? "Not grouped" : "Grouped"; + } + + internal static string GetSkillInstallStatusClass( + SkillInstallState installState, + bool hasDifferentLayoutSkills, + bool groupSkillsUnderUnityCliLoop) + { + if (installState == SkillInstallState.Checking) + { + return "setup-target-item__status--checking"; + } + + if (installState == SkillInstallState.Installed) + { + return "setup-target-item__status--installed"; + } + + if (installState == SkillInstallState.Outdated) + { + return "setup-target-item__status--outdated"; + } + + if (!hasDifferentLayoutSkills) + { + return "setup-target-item__status--missing"; + } + + return groupSkillsUnderUnityCliLoop + ? "setup-target-item__status--different-layout" + : "setup-target-item__status--different-layout"; + } + + private void UpdateSkillsStatusLabel(string text) + { + _skillsStatusLabel.text = text; + bool isVisible = !string.IsNullOrEmpty(text); + ViewDataBinder.SetVisible(_skillsStatusDivider, isVisible); + ViewDataBinder.SetVisible(_skillsStatusLabel, isVisible); + } + } +} diff --git a/Packages/src/Editor/Presentation/Setup/SetupWizardSkillsStepPresenter.cs.meta b/Packages/src/Editor/Presentation/Setup/SetupWizardSkillsStepPresenter.cs.meta new file mode 100644 index 0000000000..55265289e9 --- /dev/null +++ b/Packages/src/Editor/Presentation/Setup/SetupWizardSkillsStepPresenter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f22074d3a0cc4d2895895bc150236085 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs b/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs index e0ff7b3bd1..b83af6ff99 100644 --- a/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs +++ b/Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs @@ -199,14 +199,9 @@ private static SkillSetupUseCase GetSkillSetupUseCase() // Step 2 private VisualElement _groupSkillsRow; - private VisualElement _skillsTargetRow; private EnumField _skillsTargetField; private Toggle _groupSkillsToggle; private Label _groupSkillsLabel; - private VisualElement _skillsTargetList; - private VisualElement _skillsStatusDivider; - private Label _skillsStatusLabel; - private Button _installSkillsButton; // Footer private Toggle _suppressAutoShowToggle; @@ -217,6 +212,7 @@ private static SkillSetupUseCase GetSkillSetupUseCase() private Image _githubLinkIcon; private ScrollView _mainScrollView; private SetupWizardCliStepPresenter _cliStepPresenter; + private SetupWizardSkillsStepPresenter _skillsStepPresenter; // State private bool _isInstallingCli; @@ -311,14 +307,21 @@ private void BindElements() HandleInstallCli); _groupSkillsRow = rootVisualElement.Q("group-skills-row"); - _skillsTargetRow = rootVisualElement.Q("skills-target-row"); _skillsTargetField = rootVisualElement.Q("skills-target-field"); _groupSkillsToggle = rootVisualElement.Q("group-skills-toggle"); _groupSkillsLabel = rootVisualElement.Q