Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ new attempt. This boundary does not change release signing or Windows policy.
| Local AI model cache acquisition, explicit legacy migration, and active-path receipt selection | `HuggingFaceModelInstaller` + `LocalAiManifestStore` + `LocalAiInstallReconciler` | authoritative |
| Exact Gateway wizard terminal-restart compatibility and bounded retry policy | `GatewayWizardRestartRecoveryPolicy` | authoritative |
| Interactive onboarding routes and installation-step selection | `OnboardingFlowPolicy` | authoritative |
| Accessible provider/model and completion-card invocation | `SetupActionCard` supplies the native Invoke provider; `AiSetupPage` and `AiReadyPage` retain their guarded selection and completion actions | authoritative |
| Setup-lifetime capability/profile/explicit Custom intent/consent draft and ordered installation requirements | `SetupAccessDraft` + `SetupCapabilityProfiles`, owned by `SetupWindow` | authoritative |
| Setup provider artwork resolution and bounded page-owned download lifetime | `GatewayAiSetupPresentation` + `ProviderArtworkSession` + `ProviderArtworkLoader`, rendered by `ProviderArtwork` | authoritative |
| Native setup editor mounting and committed-result routing | `SetupWindow` through `ISetupNativeConnectionHost` + `SetupNativeConnectionPage` | authoritative |
Expand Down
17 changes: 17 additions & 0 deletions docs/ONBOARDING_WIZARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,23 @@ pages retain their large centered heroes. Each provider has one native command
surface, not a selectable ListViewItem wrapped around another clickable card.
The page-local compact row style does not alter global Toolkit resources.

Gateway-supplied provider/model choices and the final next-step cards use
`SetupActionCard`, retaining the Toolkit card visuals while supplying a real
UI Automation Invoke provider. Native UIA invocation and ordinary clicks enter
the same guarded page action. Disabled or non-clickable choices cannot activate
setup. The pinned Toolkit peer advertises Invoke without implementing its
provider, so using that peer directly is not a supported automation path.
The API Keys entry and recommended setup-link cards use the same adapter.
Their existing actions, visibility, and consent requirements are unchanged.

Mounted behavioral tests also preserve the independent ownership boundaries:
normal Gateway review hides the competing Local AI choice, changing the API
provider selection performs no activation, and the verified completion page
has no footer action that can bypass its coordinator. A completion failure
exposes recovery inside the error bar; that action returns through the owning
AI setup flow without publishing a destination. These checks exercise actual
controls and protocol effects rather than matching XAML or method text.

1. **Local AI on this PC** is a concise native choice row beside the detected
Gateway choices, with actual GPU/model facts and its trailing action. It has
no separate introductory heading or always-visible technical paragraph.
Expand Down
31 changes: 31 additions & 0 deletions src/OpenClaw.SetupEngine.UI/Controls/SetupActionCard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;

namespace OpenClaw.SetupEngine.UI.Controls;

/// <summary>Supplies the Invoke provider missing from the pinned Toolkit card peer.</summary>
public sealed partial class SetupActionCard : SettingsCard
{
public event RoutedEventHandler? InvokeRequested;

public SetupActionCard() =>
Click += (_, args) => InvokeRequested?.Invoke(this, args);

protected override AutomationPeer OnCreateAutomationPeer() => new ActionCardAutomationPeer(this);

private sealed partial class ActionCardAutomationPeer(SetupActionCard owner)
: SettingsCardAutomationPeer(owner), IInvokeProvider
{
public void Invoke()
{
if (!owner.IsEnabled || !owner.IsClickEnabled)
throw new ElementNotEnabledException();
if (!owner.DispatcherQueue.TryEnqueue(() =>
owner.InvokeRequested?.Invoke(owner, new RoutedEventArgs())))
throw new ObjectDisposedException(nameof(SetupActionCard));
}
}
}
1 change: 1 addition & 0 deletions src/OpenClaw.SetupEngine.UI/OpenClaw.SetupEngine.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<ItemGroup>
<ProjectReference Include="..\OpenClaw.SetupEngine\OpenClaw.SetupEngine.csproj" />
<ProjectReference Include="..\OpenClaw.Shared\OpenClaw.Shared.csproj" />
<InternalsVisibleTo Include="OpenClaw.Tray.UITests" />
</ItemGroup>

<!-- Reuse Setup assets from the Tray project -->
Expand Down
14 changes: 7 additions & 7 deletions src/OpenClaw.SetupEngine.UI/Pages/AiReadyPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
</InfoBar.ActionButton>
</InfoBar>
<StackPanel x:Name="Choices" Spacing="8" Visibility="Collapsed">
<tk:SettingsCard x:Name="ChatChoice" x:Uid="Onboarding_Ready_Chat" Header="Talk to my agent"
Description="Open Chat with your agent." Tag="Chat" IsClickEnabled="True" Click="Choose_Click"
<controls:SetupActionCard x:Name="ChatChoice" x:Uid="Onboarding_Ready_Chat" Header="Talk to my agent"
Description="Open Chat with your agent." Tag="Chat" IsClickEnabled="True" InvokeRequested="Choose_Click"
AutomationProperties.Name="Talk to my agent, recommended">
<controls:RecommendedBadge x:Name="RecommendedBadge" />
</tk:SettingsCard>
<tk:SettingsCard x:Name="ChannelsChoice" x:Uid="Onboarding_Ready_Channels" Header="Connect channels"
Description="WhatsApp, Telegram, and more. You choose when to sign in." Tag="Channels" IsClickEnabled="True" Click="Choose_Click" />
<tk:SettingsCard x:Name="SkillsChoice" x:Uid="Onboarding_Ready_Skills" Header="Explore skills"
Description="Browse skills for your agent. Nothing is installed automatically." Tag="Skills" IsClickEnabled="True" Click="Choose_Click" />
</controls:SetupActionCard>
<controls:SetupActionCard x:Name="ChannelsChoice" x:Uid="Onboarding_Ready_Channels" Header="Connect channels"
Description="WhatsApp, Telegram, and more. You choose when to sign in." Tag="Channels" IsClickEnabled="True" InvokeRequested="Choose_Click" />
<controls:SetupActionCard x:Name="SkillsChoice" x:Uid="Onboarding_Ready_Skills" Header="Explore skills"
Description="Browse skills for your agent. Nothing is installed automatically." Tag="Skills" IsClickEnabled="True" InvokeRequested="Choose_Click" />
</StackPanel>
<ProgressBar x:Name="BusyProgress" IsIndeterminate="True" Visibility="Collapsed" />
<TextBlock x:Name="StatusText" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="Wrap"
Expand Down
28 changes: 14 additions & 14 deletions src/OpenClaw.SetupEngine.UI/Pages/AiSetupPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
<DataTemplate x:Key="ChoiceTemplate">
<tk:SettingsCard x:Name="ChoiceActionCard" IsClickEnabled="True" Click="ChoiceAction_Click" Tag="{Binding}"
<controls:SetupActionCard x:Name="ChoiceActionCard" IsClickEnabled="True" InvokeRequested="ChoiceAction_Click" Tag="{Binding}"
Style="{StaticResource CompactChoice}"
AutomationProperties.Name="{Binding Label}">
<tk:SettingsCard.Header>
<controls:SetupActionCard.Header>
<Grid ColumnDefinitions="Auto,*">
<Viewbox Width="32" Height="32" Margin="0,0,12,0" VerticalAlignment="Center">
<controls:ProviderArtwork Descriptor="{Binding Artwork}" Session="{Binding ArtworkSession}" />
Expand All @@ -30,10 +30,10 @@
Visibility="{Binding DetailVisibility}" />
</StackPanel>
</Grid>
</tk:SettingsCard.Header>
</controls:SetupActionCard.Header>
<TextBlock Text="{Binding ActionLabel}" Style="{StaticResource BodyTextBlockStyle}"
MaxWidth="112" TextWrapping="Wrap" />
</tk:SettingsCard>
</controls:SetupActionCard>
</DataTemplate>
<DataTemplate x:Key="ProviderPickerTemplate">
<TextBlock Text="{Binding Label}" Style="{StaticResource BodyTextBlockStyle}" TextWrapping="Wrap" />
Expand Down Expand Up @@ -182,17 +182,17 @@
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="8"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="0">
<tk:SettingsCard x:Name="ApiKeysButton" IsClickEnabled="True" Click="ApiKeys_Click"
<controls:SetupActionCard x:Name="ApiKeysButton" IsClickEnabled="True" InvokeRequested="ApiKeys_Click"
Style="{StaticResource CompactChoice}">
<tk:SettingsCard.Header>
<controls:SetupActionCard.Header>
<TextBlock x:Name="ApiKeysTitle" Style="{StaticResource BodyStrongTextBlockStyle}" TextWrapping="Wrap" />
</tk:SettingsCard.Header>
<tk:SettingsCard.Description>
</controls:SetupActionCard.Header>
<controls:SetupActionCard.Description>
<TextBlock x:Name="ApiKeysDescription" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="Wrap" />
</tk:SettingsCard.Description>
</controls:SetupActionCard.Description>
<TextBlock x:Name="ApiKeysActionText" Style="{StaticResource BodyTextBlockStyle}"
MaxWidth="112" TextWrapping="Wrap" />
</tk:SettingsCard>
</controls:SetupActionCard>
<StackPanel x:Name="ApiKeyForm" Visibility="Collapsed" Spacing="12" Padding="16">
<TextBlock x:Name="ApiKeyFormHeading" Style="{StaticResource BodyStrongTextBlockStyle}" TextWrapping="Wrap" />
<Grid x:Name="ApiKeyFields" RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="*,*,Auto"
Expand Down Expand Up @@ -220,8 +220,8 @@
<ItemsControl x:Name="RecommendedInstalls">
<ItemsControl.ItemTemplate>
<DataTemplate>
<tk:SettingsCard x:Name="RecommendedInstallCard" IsClickEnabled="True" Click="RecommendedInstall_Click" Tag="{Binding Website}">
<tk:SettingsCard.Header>
<controls:SetupActionCard x:Name="RecommendedInstallCard" IsClickEnabled="True" InvokeRequested="RecommendedInstall_Click" Tag="{Binding Website}">
<controls:SetupActionCard.Header>
<Grid ColumnDefinitions="Auto,*">
<Viewbox Width="32" Height="32" Margin="0,0,12,0">
<controls:ProviderArtwork Descriptor="{Binding Artwork}" Session="{Binding ArtworkSession}" />
Expand All @@ -231,8 +231,8 @@
<TextBlock Text="{Binding Detail}" Style="{StaticResource CaptionTextBlockStyle}" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</tk:SettingsCard.Header>
</tk:SettingsCard>
</controls:SetupActionCard.Header>
</controls:SetupActionCard>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Expand Down
17 changes: 0 additions & 17 deletions tests/OpenClaw.Tray.Tests/LocalAiOnboardingOwnershipTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,23 +193,6 @@ public void SetupWindow_DrainsDepartedAiPagesAndCancelledPipelineBeforeUnlock()
Assert.Contains("if (_closed || _window?.IsClosed == true)", progress);
}

[Fact]
public void NormalGatewayReview_DoesNotOfferACompetingLocalAiSelector()
{
var source = Read(@"src\OpenClaw.SetupEngine.UI\Pages\GatewaySetupPage.xaml.cs");
Assert.Contains("LocalAiCard.Visibility = _draft.Config.LocalAi.Enabled || _window.IsLocalAiRecovery", source);
var xaml = Read(@"src\OpenClaw.SetupEngine.UI\Pages\AiSetupPage.xaml");
Assert.Contains("<tk:SettingsCard x:Name=\"LocalAiCard\"", xaml);
Assert.Contains("<tk:SettingsExpander.ItemsFooter>", xaml);
Assert.DoesNotContain("<tk:SettingsExpander.Items>", xaml);
Assert.Contains("Click=\"ChoiceAction_Click\"", xaml);
var page = Read(@"src\OpenClaw.SetupEngine.UI\Pages\AiSetupPage.xaml.cs");
var selection = page[page.IndexOf("private void Choice_Changed", StringComparison.Ordinal)..
page.IndexOf("private void ChoiceAction_Click", StringComparison.Ordinal)];
Assert.DoesNotContain("StartSelectedAsync", selection);
Assert.DoesNotContain("ContinueAsync", selection);
}

[Fact]
public void AiHeaderBand_UsesSharedCenteredArtworkAndDoesNotPromiseAContinueGate()
{
Expand Down
20 changes: 0 additions & 20 deletions tests/OpenClaw.Tray.Tests/NativeCompletionPresentationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,26 +193,6 @@ public void TimeoutPhasesHaveLocalizedUserGuidanceWithoutRenderingRawErrors(stri
Assert.DoesNotContain("error.Message", launcher);
}

[Fact]
public void ChooserHasThreeNativeActions_RecommendationAndErrorOnlyRecovery_NoFooterButtons()
{
var page = XDocument.Parse(Read(@"src\OpenClaw.SetupEngine.UI\Pages\AiReadyPage.xaml"));
var actions = page.Descendants().Where(element => element.Name.LocalName == "SettingsCard").ToArray();
Assert.Equal(["Chat", "Channels", "Skills"], actions.Select(element => element.Attribute("Tag")?.Value));
XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";
Assert.DoesNotContain(page.Descendants(), element => (string?)element.Attribute(x + "Name") is "SkipButton" or "ReturnButton");
var recovery = Assert.Single(page.Descendants(), element => (string?)element.Attribute(x + "Name") == "RecoveryButton");
Assert.Contains(recovery.Ancestors(), element => element.Name.LocalName == "InfoBar");
var badge = Assert.Single(actions[0].Descendants(), element => (string?)element.Attribute(x + "Name") == "RecommendedBadge");
Assert.Equal("RecommendedBadge", badge.Name.LocalName);
Assert.Contains(page.Descendants(), element => element.Name.LocalName == "SetupProgressIndicator");
Assert.DoesNotContain(page.Descendants(), element => (string?)element.Attribute(x + "Name") == "FinishButton");
var source = Read(@"src\OpenClaw.SetupEngine.UI\Pages\AiReadyPage.xaml.cs");
Assert.Contains("args.IsCurrent()", source);
Assert.Contains("args.Coordinator.SelectAsync(destination)", source);
Assert.DoesNotContain("Launcher.LaunchUri", source);
}

[Fact]
public void NativeConnectionProgress_HasItsOwnRowAboveWrappableActions()
{
Expand Down
Loading
Loading