V2 onboarding redesign (mount + bridge + theme + Hanselman fixes)#381
V2 onboarding redesign (mount + bridge + theme + Hanselman fixes)#381indierawk2k2 wants to merge 33 commits into
Conversation
Adds hero icons (Lobster, PartyPopper), permission row icons (Notifications, Camera, Mic, Location, ScreenCapture), checklist badges, info-bar badge, and custom title-bar chrome PNGs into Assets/Setup/ for the new V2 onboarding UX. Also commits the seven designer screen mockups under tools/v2-design-refs/ as the fixed source of truth for the upcoming visual-validation tooling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds two new projects to support the V2 setup flow inner loop:
* src/OpenClawTray.OnboardingV2/ — class library that owns the new
onboarding components, services, and widgets. Namespace
OpenClawTray.Onboarding.V2. Referenced by both the preview exe and
(at cutover) by OpenClaw.Tray.WinUI. Currently exposes
OnboardingV2App (placeholder root), OnboardingV2State, and V2Route.
* src/OpenClaw.SetupPreview/ — standalone WinUI 3 exe with no
real services and a fixed 720 x 966 dip window (aspect-matched to
the designer mocks). Headless capture mode driven by env vars:
OPENCLAW_PREVIEW_CAPTURE=1
OPENCLAW_PREVIEW_PAGE=Welcome|LocalSetupProgress|...
OPENCLAW_PREVIEW_CAPTURE_PATH=...png
OPENCLAW_PREVIEW_NODE_MODE=1
Renders the page, captures via RenderTargetBitmap, writes the PNG,
and exits with code 0 (or 1 + .error.json on failure). Smoke test:
Welcome route captures a 1414x1816 PNG ready for visual diffing.
Build wiring: explicit Program.cs Main avoids EnableMsixTooling /
Package.appxmanifest. Setup assets are linked from the Tray project so
both surfaces render byte-identical PNGs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The visual-validation discipline (see plan.md) requires a reproducible capture-and-compare pass for every page in the OnboardingV2 redesign. This tool spawns the SetupPreview exe in headless capture mode, resizes the rendered PNG to the designer canvas size, computes SSIM, mean delta-E, and connected-component bounding boxes of mismatched regions, and writes a 3-up diff.png (expected | actual | overlay) plus a report.json under out/v2-visual/<page>/. Notable design choices: * Page registry maps logical names (welcome, progress-running, progress-failed, gateway, permissions, allset, allset-no-node) to V2 routes + scenario env vars so all loop discipline runs through one CLI: 'python tools/v2_visual_diff.py --page <name>' or '--all'. * Bbox connected-component analysis short-circuits when > 25%% of pixels differ (early iterations would otherwise spend minutes on millions of micro-clusters). * to_canvas alpha-composites onto a neutral dark background so captures with transparent regions (Mica backdrop is not visible to RTB) read correctly in the side-by-side. Smoke test against the placeholder shell: SSIM 0.108, 69.7%% pixels diff (expected — page not implemented yet), 3-up diff.png renders correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The V2 shell pieces:
* OnboardingV2App is a Component<OnboardingV2State> that owns route
navigation and renders a Grid of [page area | nav bar pinned to
bottom] with a 200ms cross-fade page transition. Welcome route
hides the nav bar so the design's first impression has no chrome.
* Nav bar layout is a 3-column Grid: dot indicator pinned bottom-left,
star spacer in the middle, [Back] [Next/Finish] pinned bottom-right.
The Next button is the design accent #60C8F8 with semibold dark text,
matching Dialog-1..Dialog-5.
* StepDots widget renders Total dots, scaling the active one to 10px
with the accent colour and the others to 8px in #5A5A5A.
* Five page placeholders (Welcome, LocalSetupProgress, GatewayWelcome,
Permissions, AllSet) are wired in with the canonical PageOrder so
routing + dot indicator + nav bar visibility all behave correctly.
Real page content lands in subsequent page-* todos and is gated by
the matching vv-* visual-validation loop.
PreviewWindow updates:
* Replaces MicaBackdrop with a flat #202020 SolidColorBrush so
RenderTargetBitmap captures the real shell instead of a Mica-
transparent black hole.
* Adds a custom XAML title bar (lobster icon + 'OpenClaw Setup' text)
via ExtendsContentIntoTitleBar + SetTitleBar; styles the system
min/max/close caption buttons to the dark palette.
* Window is sized 720 x 885 dip, an 0.813 aspect ratio that matches
the 2010 x 2472 designer mock canvas exactly.
App.xaml now sets RequestedTheme=Dark so TextBlocks render light-on-dark
without per-element foreground overrides.
Smoke vv check on progress-running: SSIM 0.91, layout matches at the
shell level; remaining diff is page content that lands later.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Welcome page (Dialog.png) lays out the lobster hero, title, body copy, info card with blue (i) badge, primary 'Set up locally' accent button (focus ring suppressed), and 'Advanced setup' hyperlink, in the designer-matched proportions. Side-by-side review against Dialog.png shows content + colour + structure matching; remaining differences are designer mock-canvas decorations (~50px outer glow + drop shadow) that don't appear in the real app. v2_visual_diff.py simplified: dropped SSIM, scipy, scikit-image, and the PASS/FAIL gate. The tool now captures the preview and renders a clean 2-up side-by-side (designer | actual). Pixel-level metrics were tried and produced more noise than signal — the designer mock canvas shadow created a systematic positional offset that drowned out real diffs, and font anti-aliasing flagged false positives even after cropping. The validation gate is now Copilot's eyes on the side-by-side, with snapshot-regression deferred to cutover for the separate problem of 'catch unintentional changes vs approved render'. Title-bar lobster bumped to 18px UniformToFill so it actually reads as a lobster instead of a blurry smudge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the seven-stage checklist page for the local-setup flow:
* Title 'Setting up locally' + subtitle 'Creating OpenClaw Gateway
WSL instance', both centered.
* Seven left-aligned stage rows (Check system, Installing Ubuntu,
Configuring instance, Installing OpenClaw, Preparing gateway,
Starting gateway, Generating setup code), each with a status
badge on the right.
* Status badges:
- Done \u2192 24px green (#2BC36F) circle with white tick
- Running \u2192 24px ProgressRing tinted #60C8F8
- Failed \u2192 24px pink (#F4A6B0) circle with dark X
- Idle \u2192 24px transparent placeholder (preserves alignment)
* Inline error card with maroon background (#3D1818), body copy +
'Try again' button (#551B20 bg), slides directly under the failed
row. Driven by state.LocalSetupErrorMessage being non-null.
PreviewWindow gains env-driven fake state so the visual loop can
exercise both frames without a real engine:
OPENCLAW_PREVIEW_PROGRESS_FROZEN_STAGE=PreparingGateway
OPENCLAW_PREVIEW_FAIL_STAGE=StartingGateway
Shell fix: the dot indicator counts only the chromed pages (4, not 5)
so it matches Dialog-1..Dialog-5 — Welcome has no dot. Active index is
pageIndex-1 clamped to 0.
Side-by-side review against Dialog-1 and Dialog-6 confirms title,
subtitle, all seven rows, all three badge styles, error card, and
Try-again button all match the designer.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renders the gateway welcome card after local setup completes: title 'Configuring gateway' + dark card (#2C2C2C, 8px radius) containing the welcome heading, two paragraphs explaining the local URL and data-stays-local privacy posture, and an external-link hyperlink 'Open http://localhost:18789 in browser' below the card. Fixes designer-canvas typos in our copy: localhost18789 -> http:// localhost:18789, 'Stays' -> 'stays'. Real Launcher.LaunchUriAsync wiring lands at cutover; the preview link is a no-op. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Five-row permission status list with the all-granted scenario:
* Title 'Grant permissions' + intro paragraph centered.
* Five row cards (Notifications, Camera, Microphone, Location
optional, Screen Capture). Each card: 28px icon + title (15pt
SemiBold) + green 'Enabled' status. The Screen Capture row uses
'Available - uses picker per capture' status instead of the
Open Settings link, matching Dialog-5.
* 'Refresh status' button bottom-right under the list.
* Card bg #2C2C2C with 8px corner radius; status colour #6DC868
accent green.
Real PermissionChecker wiring lands at cutover; for now the row data
is hard-coded to all-granted (and OPENCLAW_PREVIEW_PERMS_SCENARIO is
plumbed into PreviewWindow but currently only that one scenario is
implemented).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AllSetPage: party-popper hero, `All set!` title, optional Node-Mode warning band, and Launch-at-startup row with explicit On/Off label to the left of the toggle (matches designer Dialog-4). v2_visual_diff.py: drop dead duplicate SSIM code below the live main(), and always run an incremental `dotnet build` once per invocation so live code edits are picked up automatically (cached for --all so it builds once, not per page). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a small `V2Animations` static helper that wires WinUI Composition animations onto FunctionalUI elements via the .Set escape hatch: * Welcome lobster: continuous breathing pulse (1.0 -> 1.025 -> 1.0 every ~4.2s). * Welcome info card: 360ms fade-in delayed 200ms after page load. * AllSet party popper: 520ms scale pop-in (0.6 -> 1.05 -> 1.0) + fade. * Progress error card: 320ms slide-in from 14px below + fade. All animations are gated by V2Animations.DisableForCapture, which the SetupPreview headless capture path sets to true. This keeps the visual diff PNGs deterministic regardless of animation phase, so the LLM-eval validation loop is not flaky. Verified by re-running --all and confirming all 7 captures still match the designer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reduce custom title-bar lobster from 18px Stretch.UniformToFill to 14px Stretch.Uniform with an 8px right margin, drop the lead-in padding from 16 to 14px, and add AutomationProperties.Name so screen readers announce the custom chrome correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add OpenClawTray.Onboarding.V2.V2Strings — a static key-+-default-English dictionary with a settable Resolver delegate. Pages call V2Strings.Get("V2_...") and the resolver defaults to the built-in English dictionary so SetupPreview and unit tests work with no ResourceManager wired. At cutover the Tray host overrides Resolver = LocalizationHelper.GetString and adds the keys to all five .resw files.
Replace every hard-coded English string in OnboardingV2 (Welcome, LocalSetupProgress, GatewayWelcome, Permissions, AllSet pages, plus the shell nav buttons) with V2Strings.Get(...) calls. Fixed designer typos in the process: `localhost18789` -> `http://localhost:18789`, `Stays` -> `stays` in Dialog-2; `Acvtive` is intentionally kept in the *designer* reference PNG but my source string already says `Active`.
Visual sweep: --all confirms every captured page still matches the designer mock.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- OnboardingV2State: add StateChanged event, property setters notify, plus cutover-staged shape (GatewayUrl, GatewayHealthy, LaunchAtStartup, Permissions snapshot list with PermissionRowSnapshot record and PermissionSeverity enum) so service wiring lands without re-shaping. - OnboardingV2App: subscribe to StateChanged in UseEffect and bump a renderTick UseState so V2 re-renders when services mutate state. - V2Strings.Get: fall back to DefaultEnUs when resolver returns null, empty, or echoes the key, so V2 never renders raw resource keys during incremental localization rollout. - Animations: introduce ShouldAnimate predicate honoring both DisableForCapture and UISettings.AnimationsEnabled (reduce-motion). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Re-enable system focus visuals on every V2 button (was UseSystemFocusVisuals=false on Welcome Set-up-locally, Gateway Advanced, Permissions Refresh & Open-Settings, Progress Try-again). Keyboard users now get the standard cyan focus ring. - Add AutomationProperties.AutomationId to nav Back/Next/Finish (Next vs Finish decided per page, so the id reflects the current label) and to every page CTA (Set up locally, Advanced setup, Refresh status, Open Settings, Try again, Launch-at-startup toggle). Stable ids unblock UI automation. - Add AutomationProperties.Name on the AllSet startup toggle (uses the visible ''Launch OpenClaw at startup?'' string so screen readers announce intent). - PreviewWindow: replace the hard-coded 138 DIP title-bar right padding with a RightInset-derived value. Reads AppWindow.TitleBar.RightInset (physical px), divides by XamlRoot.RasterizationScale, and re-applies on AppWindow.Changed. Falls back to 138 DIP if either lookup is unavailable. Visual sweep (python tools/v2_visual_diff.py --all) reproduces identical output; focus rings only appear on keyboard focus and are absent in capture-mode PNGs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Captures the rubber-duck critique outputs as a follow-up-PR-ready spec: - Where the V2 code lives (project map: OnboardingV2 lib + SetupPreview exe). - How the inner loop works (edit -> v2_visual_diff.py -> view PNG -> iterate). - Cutover plan: OnboardingWindow mount swap, Wizard-step decision, stage-map reuse, advanced-setup wiring, completion remap, state plumbing table, .resw rollout to all 5 locales. - Animation discipline: ShouldAnimate predicate gates DisableForCapture + reduce-motion (UISettings.AnimationsEnabled). - Accessibility checklist: focus visuals, AutomationIds, AutomationName, + remaining manual gates (keyboard nav, screen-reader smoke). - Visual validation: how to run, what to inspect, design typo policy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-enabling system focus visuals on V2 buttons (a11y improvement in the previous commit) caused the first focusable in tab order to render with a cyan focus ring in capture-mode PNGs. Visible on the gateway page (Open ... in browser link) and the progress-failed page (Try again). Fix: in CaptureAndExitAsync, add a zero-size, non-hit-testable, non- opaque ContentControl as a focus sink, programmatically focus it, give it one frame to settle, then RenderTargetBitmap and remove. Captures are deterministic again; live UI focus visuals are unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… fake timer) 5 bugs surfaced from manual walkthrough of the live preview: 1. Nav dots clipped/shifted left vs comp. Replaced Margin(40,0,0,0) on the dots and Margin(0,0,40,0) on the buttons with a uniform Padding(60,24,60,32) on the nav bar so both ends of the chrome sit ~60 DIP inside the dialog. 2. No way to see the LocalSetupProgress checklist actually progress. Added a DispatcherQueueTimer in interactive (non-capture) mode that starts the first stage spinning, then promotes one row at a time (~900 ms per step) and loops at the end so a designer can keep watching. Skipped when the PROGRESS_FROZEN_STAGE / FAIL_STAGE env vars are set (deterministic capture). 3. Window had square corners. Setting SystemBackdrop=null (so captures aren''t transparent over Mica) also unhooked Windows 11''s default rounding. Added TryApplyRoundedCorners() that sets DwmSetWindowAttribute with WINDOW_CORNER_PREFERENCE = ROUND on the window HWND. Silent no-op on Windows 10 / failure. 4. Permissions page used Unicode arrow / refresh glyphs (\u2197, \u21BB) which rendered tiny vs the comp. Replaced both with Segoe Fluent Icons via a new GlyphButtonContent helper: Open Settings uses \uE8A7 (OpenInNewWindow, square-with-arrow) and Refresh status uses \uE72C (Refresh, circular arrow), both at 16pt to match Dialog-5. 5. AllSet defaulted NodeModeActive=false so the amber Node-Mode-Active card was hidden in the live preview. The design treats Node Mode as the default for a Windows tray install, so PreviewWindow now seeds NodeModeActive=true before ApplyEnvOverrides (env override still wins). Visual sweep regenerated; permissions and allset captures now show the new icons / amber card. Spinner on progress-running is a capture-mode artifact (ProgressRing snapshot mid-frame); live UX shows it animated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* V2Theme.cs centralises every theme-aware brush used by the V2 pages:
window/card backgrounds, primary/secondary/subtle text, accents (cyan,
green, badge pink/amber), error and warning cards, transparent button
overlays, step-dot inactive colour. Dark values mirror the designer
mocks; light values pattern off WinUI 3 Fluent defaults. Brand accents
stay constant across themes per the designer''s spec. Brushes are
memoised per (role, theme) tuple.
* OnboardingV2State adds V2ThemeMode (System/Light/Dark) for the user''s
preference and EffectiveTheme (ElementTheme) for the resolved value.
Both raise StateChanged. Pages read EffectiveTheme in Render() and
forward it to V2Theme accessors so the visual tree re-renders on
theme change. EffectiveTheme coerces Default -> Dark.
* All five V2 pages + OnboardingV2App nav bar + StepDots widget
refactored to look up colours via V2Theme. No more hard-coded
ColorHelper.FromArgb or string-hex backgrounds inside page code.
* PermissionsPage row icons now sit inside a constant-colour dark
circular badge so the designer''s white-on-dark PNGs stay visible on
light-theme white cards. Badge is 40x40 with 24x24 icon, transparent
in dark mode (the icon already reads on the dark card).
* GatewayWelcomePage and WelcomePage''s Advanced setup link now invoke
Props.RequestAdvancedSetup() (new event) so the cutover bridge can
route to the legacy Connection page. Welcome page reads
Props.LaunchAtStartup as the initial toggle state and mirrors local
state back into the shared state on change.
* PreviewWindow drives theme end-to-end: reads OPENCLAW_PREVIEW_THEME
env var (System/Light/Dark) into state.ThemeMode, listens to Windows
app-mode color changes via UISettings.ColorValuesChanged, and adds
F2 to cycle System -> Light -> Dark interactively. ApplyResolvedTheme
pushes the resolved ElementTheme onto state.EffectiveTheme +
root grid background + RequestedTheme (so WinUI built-in chrome flips)
+ system caption buttons.
* App.xaml drops the hard-coded RequestedTheme=Dark so
Application.Current.RequestedTheme tracks the Windows app-mode setting.
* tools/v2_visual_diff.py adds --theme {System,Light,Dark} so designer
feedback can compare both modes against the (dark-only) reference PNGs.
Visual sweep verified: dark renders identical to the original designer
mocks; light renders cleanly with white cards, dark text, theme-aware
accents, and visible permission icons.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires OnboardingV2App into the live app. OnboardingWindow now mounts the
V2 component tree by default; set OPENCLAW_USE_V2_SETUP=0 to fall back
to the legacy flow as a kill-switch for one cycle (also flips to legacy
when OPENCLAW_ONBOARDING_START_ROUTE=Connection so the Advanced fallback
works).
* OnboardingV2Bridge: new class that synchronises real tray services to
OnboardingV2State.
- LocalGatewaySetupEngine.StateChanged -> V2State.LocalSetupRows +
LocalSetupErrorMessage, using the existing
LocalSetupProgressStageMap so V2 picks up every bug-fix that already
landed against the legacy stage computation (rather than forking).
- PermissionChecker.CheckAllAsync + SubscribeToAccessChanges ->
V2State.Permissions (PermissionRowSnapshot list).
- SettingsManager.GetEffectiveGatewayUrl -> V2State.GatewayUrl (with
ws://->http:// scheme flip so the browser link is launch-able).
- SettingsManager.AutoStart <-> V2State.LaunchAtStartup (two-way:
initial value pulls from settings; toggle change persists back via
SettingsManager.Save).
- V2State.AdvanceRequested while on Welcome -> kick off the engine
exactly once.
- V2State.AdvancedSetupRequested + V2State.Finished surfaced as
bridge events for the host to handle.
All cross-thread mutations marshal through DispatcherQueue.TryEnqueue.
* OnboardingV2State.LaunchAtStartup setter raises a dedicated
LaunchAtStartupChanged event so the bridge only persists when the
toggle actually flips (subscribing to StateChanged would re-fire on
every progress tick).
* OnboardingWindow:
- New _useV2 path that constructs the V2 state + bridge and mounts
OnboardingV2App via the same FunctionalHostControl.
- Routes V2Strings via V2Strings.Resolver = LocalizationHelper.GetString
so the new UI reads from the same .resw resources as legacy.
- TryCompleteOnboarding now recognises V2Route.AllSet as the terminal
page (in addition to legacy OnboardingRoute.Ready).
- OpenLegacyAdvancedSetup re-mounts the host onto OnboardingApp at
OnboardingRoute.Connection so Welcome's Advanced setup link drops
the user straight into the legacy Connection page (per design
decision: Advanced page hasn''t been redesigned yet).
- Bridge is disposed on Closed so the engine subscription is
released cleanly.
* Localization: tools/seed_v2_resw.py parses V2Strings.DefaultEnUs and
appends every V2_* key to all five .resw locale files with the English
value (210 entries total, idempotent). The translate-or-invariant test
is taught that V2_* keys are intentionally English-only at first ship
(translations are a follow-up). All other localization tests stay green.
Validation:
build.ps1 -> all four projects green.
Shared.Tests -> 1548 passed / 28 skipped / 0 failed.
Tray.Tests -> 1164 passed / 0 failed.
Deferred to a follow-up PR (documented in docs/ONBOARDING_V2.md):
- Fold legacy Wizard provider/model picker into V2 GatewayWelcome.
- Real translations for V2 strings.
- Delete legacy OnboardingApp + Pages (kept for Advanced setup).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Moves the cutover plan into a 'this PR' section (services wired, OnboardingWindow mount swap, kill-switch env var, AdvancedSetup remounts legacy in place). Adds a follow-up backlog covering the Wizard fold, legacy page deletion, real V2 string translations, kill-switch removal, and the Advanced -> AllSet round-trip wiring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ns wiring + bridge-back Fixes manual-test feedback: * Bug 1 (theme): V2 was rendering Dark while titlebar buttons were Light because Application.Current.RequestedTheme returns Light on unpackaged WinUI 3 apps regardless of the Windows app-mode setting. New V2SystemTheme helper reads the actual system theme via UISettings foreground colour (white in dark mode, black in light) and the bridge applies it on construction + on UISettings.ColorValuesChanged. * Bug 3 (gateway wizard): GatewayWelcome page now embeds the legacy WizardPage (provider/model RPC picker) when the host provides a GatewayWizardChildFactory. Avoids a circular project reference by having the host (OnboardingWindow, in Tray.WinUI) build the factory closure that returns the WizardPage element. V2 GatewayWelcome renders the welcome card + the wizard child + the open-in-browser link. * Bug 4 (permissions wiring): V2 PermissionsPage now reads Props.Permissions (real PermissionRowSnapshot data) when populated and falls back to the preview-only AllGranted rows when null. Open Settings actually launches the ms-settings:// URI via Windows.System.Launcher (with a scheme guard matching the legacy security gate). Refresh status raises a new PermissionsRefreshRequested event the bridge subscribes to, which re-runs PermissionChecker.CheckAllAsync. * Bug 5 (NodeMode default): bridge now seeds V2State.NodeModeActive=true for the local easy-setup default (matches design). Engine completion re-syncs from Settings.EnableNodeMode (which PairAsync flips mid-onboarding) so the AllSet page always shows the amber Node-Mode card on the local path. * Bug 6 (Advanced -> V2 round-trip): OpenLegacyAdvancedSetup keeps _v2State alive and arms _v2BridgeBackPending. When legacy state.RouteChanged fires past Connection, OnRouteChanged re-mounts V2 at the closest equivalent route (Wizard -> GatewayWelcome, Permissions -> Permissions, Ready -> AllSet). User completes legacy Connection then continues in V2 chrome with the wizard fold and the V2 permissions / all-set screens. * Bug 2 (slow check-system spin) is intrinsic engine timing on a freshly wiped WSL host — Preflight + EnsureWslEnabled actually take ~10s on first install. Not addressable from V2 without changing the engine. Validation: build.ps1 -> all four projects green. Tray.Tests -> 1164 passed / 0 failed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rd + existing-config warn-and-confirm
* V2 GatewayWelcome: removed the welcome intro card and Open-in-browser
link so the embedded legacy WizardPage has the full vertical space
and its own nav buttons are reachable. The intro card returns in a
follow-up PR alongside a V2-styled wizard rewrite.
* Bridge-back from legacy Advanced now skips the legacy Wizard step.
Advanced setup assumes the user is connecting to an already-configured
gateway, so all of Wizard/Permissions/Ready map to V2 Permissions
(was: Wizard -> V2 GatewayWelcome which re-prompted for provider/model).
Ready still maps to V2 AllSet.
* OnboardingV2App no longer overwrites Props.CurrentRoute from its own
pageIndex on every render -- that was clobbering external navigation
requests from the host bridge. The component now treats Props.CurrentRoute
as a one-shot navigation cue: when it changes from the previously
observed value, V2 re-syncs pageIndex to follow Props (and pushes
nav.Navigate so transition animations still fire). User-driven
next/back continues to mutate pageIndex without writing back to Props.
* V2 Welcome handles the existing-config case (same UX as legacy
SetupWarningPage):
- Bridge populates _v2State.ExistingConfig from OnboardingExistingConfigGuard.GetSummary()
- When ExistingConfig.HasAny is true and replace not confirmed, the
bottom cluster swaps to an amber warning card with the dynamic
lost-items list, a primary Replace my setup accent button, and a
Keep my setup link that returns to the normal welcome state.
- Confirmation sets V2State.ReplaceExistingConfigurationConfirmed; the
bridge forwards it to OnboardingState.ReplaceExistingConfigurationConfirmed
before starting the engine so the existing-config guard accepts the run.
Advanced setup link stays visible in both states.
* New V2 strings (V2_Welcome_Replace_Heading / Body / Confirm / Keep)
seeded into all five .resw locales with English values via
tools/seed_v2_resw.py.
Validation: build.ps1 + Tray.Tests (1164 / 0) green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…th in V2 nav Engine completed (Phase=Complete, Status=Complete) at the pairing step but V2 stayed on LocalSetupProgress. Root cause: a previous fix removed the pageIndex -> Props.CurrentRoute write in OnboardingV2App to stop V2 from clobbering external nav requests, but that left two sources of truth that drifted out of sync. The bridge's auto-advance check (Props.CurrentRoute == V2Route.LocalSetupProgress) evaluated false even when V2 was on that page, because Props.CurrentRoute still held the last externally-set value (Welcome on first mount). Fix: collapse to a single source. Props.CurrentRoute is the source of truth; pageIndex is derived from Array.IndexOf on every render. GoNext and GoBack mutate Props.CurrentRoute directly (which fires StateChanged, re-renders with a fresh pageIndex). External code (the bridge) sets Props.CurrentRoute and gets the same re-render path. nav.Navigate is driven by a per-render "did the route change since last seen?" check so the NavigationHost transition still fires. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… logging The previous Props.CurrentRoute fix made the bridge's auto-advance guard correct, but a second hang reproduced anyway. Without observable state I couldn't tell whether the engine StateChanged event was reaching the bridge, whether DispatchToUi was firing the lambda, or whether the CurrentRoute check was failing. This change: * Belt-and-braces auto-advance via _runTask.ContinueWith on RunLocalOnlyAsync. When the task completes we manually invoke OnEngineStateChanged with the final state so we always see Status=Complete even if the StateChanged event chain dropped the final tick (cross-thread subscription, GC, marshaling timing, etc.). * Loosened the auto-advance gate to also accept CurrentRoute=Welcome in case engine completion races V2's first navigation. * Removed the redundant _state.RequestAdvance() after setting CurrentRoute. With the single-source-of-truth model, mutating CurrentRoute is enough; calling RequestAdvance afterwards would double-advance (Welcome -> Local -> GatewayWelcome -> Permissions on a single completion event). * Logger.Info markers in EnsureEngineStarted, OnEngineStateChanged, and inside the dispatched lambda so future hangs are diagnosable from openclaw-tray.log. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The V2 bridge had drifted from the v1 LocalSetupProgressPage in several
load-bearing ways. User caught this and asked for a strict "only paint
pixels" stance on the easy-button path. This commit restores v1
behaviour 1:1 in the bridge while leaving the V2 visuals untouched.
Restored logic (matches v1 LocalSetupProgressPage line-for-line):
* replaceConfirmed flag forwarded from V2 state to engine factory and
to legacy OnboardingState (was hard-coded true in the V2 path; now
passes through the user's actual choice from the V2 Welcome
warn-and-confirm).
* Pre-advance gateway client re-init on Status=Complete:
- Calls App.ConnectionManager.ReconnectAsync() if App.GatewayClient
is null or disconnected. PairAsync flips EnableNodeMode mid-
onboarding, which prevents the operator client's auto-connect at
startup.
- Seeds legacyState.GatewayClient = App.GatewayClient so the embedded
WizardPage finds it.
- Without this the wizard sat in "loading" for 30s then went offline.
* _advanceFiredForCompletion guard - first Complete event triggers the
advance; subsequent Complete events (from the StateChanged stream
and the ContinueWith fallback) are ignored.
* 1-second pause before advance for visual settling (Mike's UX choice
in v1).
* Existing-config defense check - re-validates legacy
OnboardingExistingConfigGuard.HasExistingConfiguration() before
starting the engine, surfacing a synthetic Block message when
replace was not confirmed (parity with v1's existing_config_gate).
* engine_construct_failed surfacing - if _engineFactory throws, we now
set a user-facing error message on V2State.LocalSetupErrorMessage so
the V2 progress page renders a Try-again card (parity with v1's
Block-state synthesis).
* lastRunningPhase derivation rewritten to scan state.History (most
recent non-Failed/Cancelled/NotStarted phase) instead of tracking
locally during run. Matters on failure: when Phase rolls to Failed,
the local tracker would still hold the pre-failure phase but History
is the canonical source.
The bridge already keeps the engine alive across V2 navigations (the
bridge survives the entire onboarding window), so the s_engine static
pattern from v1 is unnecessary here.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Hanselman adversarial review (Opus + Codex) flagged 9 issues. This commit
fixes all 4 HIGH-consensus items, the Codex-only Permissions notify bug
(verified real and promoted to HIGH), and the Codex-only enum-index cast
hardening. Three LOW-consensus items deferred (notes in docs):
* rf-permissions-static-init -- harmless today; preview-only fallback rows.
* rf-completionpolicy-v2 -- harmless today; engine must succeed before
V2 AllSet renders.
* rf-webview-handler-stack -- pre-existing in master, not from this PR.
Fixes in this commit:
1. rf-bridge-recreate (Opus CRITICAL / Codex HIGH)
OpenLegacyAdvancedSetup() disposed _v2Bridge and OnRouteChanged
re-mounted V2 without recreating it, so after the Advanced -> V2
round-trip the V2 Finish, AdvancedSetup, Refresh, AutoStart-persist,
and engine-start events all had zero subscribers. Extracted bridge
construction + event wiring into CreateAndStartV2Bridge(settings) and
call it from both the initial mount path and the bridge-back path.
Idempotent (disposes any prior bridge first).
2. rf-permissions-notify (Codex HIGH; Opus missed)
OnboardingV2State.Permissions was an auto-property whose setter never
raised StateChanged. The bridge updates the snapshot list but V2
never re-rendered, so users always saw the preview AllGranted rows
with no-op buttons. Converted to backing field with NotifyChanged()
in the setter.
3. rf-tryagain-noop (Opus HIGH / Codex MEDIUM)
"Try again" button on the LocalSetupProgress error card was a stub
() => {}. Added a RetryRequested event on V2State, plumbed it through
to the bridge's OnRetryRequested handler which detaches the prior
engine's StateChanged subscription, resets _engineStarted /
_advanceFiredForCompletion / _runTask / _lastRunningPhase, clears the
error message, marks all stages idle, and calls EnsureEngineStarted()
to re-run.
4. rf-uisettings-leak (both MEDIUM)
PreviewWindow stored UISettings in a local var; the
ColorValuesChanged subscription kept a strong COM reference to the
lambda (and via it, the window), preventing GC. Moved both the
UISettings instance and the handler delegate to fields, and
unsubscribe in a Closed handler. Matches the pattern already used in
OnboardingV2Bridge.Dispose().
5. rf-nodemode-latch (Opus LOW / Codex MEDIUM)
Bridge was doing NodeModeActive = settings.EnableNodeMode ||
NodeModeActive -- a one-way OR latch that never returned to false.
Replaced with direct assignment so settings is the single source of
truth.
6. rf-stage-enum-cast (Codex LOW)
MapToV2Rows used (V2Stage)i casting from VisibleStages index. If
either list was reordered, the mapping silently shifted across all 7
stage siblings (CheckSystem ... GeneratingSetupCode). Replaced with
an explicit StageOrder array and a length-mismatch InvalidOperationException
guard at the top of the method so reorder breaks loud, not silent.
Validation:
build.ps1 -> all 4 projects green
Shared.Tests -> 1548 passed / 28 skipped / 0 failed
Tray.Tests -> 1164 passed / 0 failed
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@shanselman ready for review when you have a chance — V2 onboarding redesign is mounted in the app, easy-button path is logic-identical to v1 master, Hanselman dual-model review findings (5 HIGH consensus) all addressed. PR body has the full architecture + service-wiring table + manual test results. Thanks! |
|
Requesting changes on the Keep my setup path because this regresses the onboarding bug we just chased down and fixed in #375. Context: after #340, we proved that “Keep my setup” must be a true dismiss/preserve action: close onboarding without completing setup, without touching settings/autostart, and without continuing into local setup. #375 then fixed the remaining startup gate so node-mode returning users with per-gateway PR #381 reintroduces the same class of bug in the V2 flow: // src/OpenClawTray.OnboardingV2/Pages/WelcomePage.cs
void KeepSetup() => setConfirmingReplace(false);That only hides the warning card. The user then gets the normal V2 CTA cluster again, including Set up locally. If they click it, There is also a legacy fallback regression in Suggested fix:
How to avoid this going forward: treat “replace existing setup / keep existing setup / advanced setup” as shared onboarding policy, not page-local UI state. Both V1 and V2 should call the same host-level actions for Replace, Keep, and Advanced, and tests should cover those host-level actions rather than only checking the visual page state. This is exactly the kind of lifecycle behavior that gets lost when a redesign reimplements the buttons independently. |
|
Additional findings from the deep review, separate from the Keep my setup regression above. 1. Stale retry continuation can auto-advance from an old setup run
_runTask = _engine.RunLocalOnlyAsync();
_runTask.ContinueWith(t =>
{
if (t.IsCompletedSuccessfully && t.Result is { } finalState)
{
OnEngineStateChanged(finalState);
}
}, TaskScheduler.Default);If a user clicks Try again while the old task is still completing, the stale continuation can observe Suggested fix: add an engine generation/run id and ignore stale callbacks. private int _engineGeneration;
private void OnRetryRequested(object? sender, EventArgs e)
{
_engineGeneration++;
...
}
private void EnsureEngineStarted()
{
...
var generation = _engineGeneration;
_runTask = _engine.RunLocalOnlyAsync();
_runTask.ContinueWith(t =>
{
if (generation != _engineGeneration) return;
if (t.IsCompletedSuccessfully && t.Result is { } finalState)
{
OnEngineStateChanged(finalState);
}
}, TaskScheduler.Default);
}Even better if the engine supports cancellation/disposal: cancel the old run before starting a new one, and still keep the generation guard as defense-in-depth. 2.
|
…+ wire V2 dismiss Our V2 branch was cut at a03c454 (before PR openclaw#340 landed in master). PR openclaw#340 fixed two real bugs in the legacy onboarding flow: "Keep my setup" didn't actually dismiss the wizard, and ExistingConfigGuard only scanned the legacy root identity dir for operator tokens (per-gateway tokens under gateways/<id>/device-key-ed25519.json were missed). PR openclaw#375 (Scott's, still open) extends the per-gateway scan symmetrically to the node-token role, fixing a startup-relaunch-reopens-onboarding bug for local node-mode profiles whose node token lives only per-gateway. This commit: 1. Merges origin/master to absorb PR openclaw#340's three commits (Dismissed event/method on OnboardingState, OnboardingWindow.OnOnboardingDismissed handler, SetupWarningPage CancelReplace -> Props.Dismiss, removed default-to-Advanced in returning-user path, HasAnyOperatorDeviceToken per-gateway scan, StartupSetupState rewrite). Auto-resolved a single conflict in OnboardingWindow.cs via the ORT strategy. 2. Applies PR openclaw#375's symmetric per-gateway scan locally: - Refactored OnboardingExistingConfigGuard.HasAnyOperatorDeviceToken to delegate to a new HasAnyDeviceTokenForRole(dataPath, role). - GetSummary now uses HasAnyDeviceTokenForRole for the node side too, not just operator (the asymmetric defect that PR openclaw#375 catches). - StartupSetupState.HasStoredNodeDeviceToken now delegates to the guard's per-gateway-aware helper so the startup auto-launch decision and the in-wizard guard agree. 3. Wires V2 equivalents of PR openclaw#340's Dismiss machinery so the V2 redesigned UI gets the same fix, not just the legacy UI: - OnboardingV2State adds Dismissed event + idempotent Dismiss() method (matches legacy OnboardingState.Dismiss semantics: log, fire once). - V2 WelcomePage's "Keep my setup" handler now calls Props.Dismiss() instead of just toggling setConfirmingReplace(false). Mirrors legacy SetupWarningPage CancelReplace post-PR-openclaw#340. - OnboardingV2Bridge surfaces a Dismissed event (subscribes to V2 state, forwards to host). - OnboardingWindow's V2 mount now subscribes to bridge.Dismissed and reuses the existing _dismissedWithoutCompletion flag + Close pattern from PR openclaw#340's OnOnboardingDismissed so OnClosed skips TryCompleteOnboarding the same way for both legacy and V2 dismiss paths. Validation: build.ps1 -> all 4 projects green Shared.Tests -> 1548 passed / 28 skipped / 0 failed Tray.Tests -> 1178 passed / 0 failed (was 1164 -- +14 from PR openclaw#340's OnboardingExistingConfigGuardTests + OnboardingStateTests + StartupSetupStateTests new coverage we just absorbed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@shanselman thanks for the keepSetup() heads up. Pushed 488c94f which:
Validation: build green; Shared.Tests 1548/0/28 skipped; Tray.Tests 1178/0 (+14 from #340's new coverage). |
|
Re-reviewed latest head What I verified as fixed:
Local validation on this head passed:
Still requesting changes on these remaining items: 1.
|
Both Windows clients hard-coded `minProtocol=3, maxProtocol=3` in their connect handshake. The OpenClaw gateway service installed by `LocalGatewaySetupEngine` uses `OpenClawInstallVersion = "latest"`, which now requires protocol v4 (returns `expectedProtocol:4, minimumProbeProtocol:4` and refuses any client that doesn't advertise v4 in its connect range). Result: every fresh local-WSL setup hangs on the operator pairing phase and times out with `operator_pairing_timeout` after the engine keeps retrying the handshake against a server that won't accept v3. Fix: bump `maxProtocol` to 4 in both clients while keeping `minProtocol=3` so we still negotiate down for older gateways that haven't been updated. Standard backward-compatible negotiation window. Two-line change in two files: - src/OpenClaw.Shared/OpenClawGatewayClient.cs (operator client) - src/OpenClaw.Shared/WindowsNodeClient.cs (node client) The signed connect payload (`BuildConnectPayloadV3` / `SignConnectPayloadV3`) is unchanged — v3 signatures remain valid against v4 gateways (verified against a freshly-installed `latest` gateway via WebSocket probe: server accepts the connection and sends `connect.challenge` for both `maxProtocol=3` and `maxProtocol=4` clients; only the subsequent auth handshake requires v4 in the range). Validation: build.ps1 -> all 4 projects green Shared.Tests -> 1548 passed / 28 skipped / 0 failed Tray.Tests -> 1178 passed / 0 failed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two LOW-consensus items from the second adversarial review pass: 1. OnboardingV2Bridge.OnDismissed UI-thread marshalling (Codex) Wrap the Dismissed forwarder in DispatchToUi so future callers that raise Dismiss from a background thread (engine event, watchdog) cannot crash WinUI by closing the host window off-thread. Today's only call path is the Welcome page button click, so this is a defensive no-op fast path — but it preserves the contract "Dismissed always fires on the UI thread for the host" symmetrically with how OnFinished is set up to be invoked via state events that are already UI-thread. 2. OnboardingV2State.Dismiss symmetric test coverage (Opus) Legacy OnboardingState has 4 Dismiss_* tests covering fires-once, idempotency, no-Finished-cross-fire, and no-handler safety. The V2 state surface uses the same idempotent-flag pattern but had no sibling tests. Add OpenClawTray.OnboardingV2.Tests with 4 mirror tests so the symmetric contract is enforced in CI. The new test project targets net10.0-windows10.0.22621.0 to match OnboardingV2 (which depends on Microsoft.UI.Xaml.ElementTheme and therefore cannot be Compile-Included into the pure-net10.0 Tray.Tests project). InternalsVisibleTo for OpenClawTray.OnboardingV2.Tests was already declared on the V2 csproj — the project name was anticipated. Validation (worktree, OPENCLAW_REPO_ROOT set): - ./build.ps1 — all 4 projects ✅ - Shared.Tests — 1548 passed / 28 skipped / 0 failed ✅ - Tray.Tests — 1178 passed / 0 failed ✅ - OpenClawTray.OnboardingV2.Tests — 4 passed / 0 failed ✅ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Four items flagged by @shanselman after re-reviewing 488c94f. All four addressed in this commit: 1. _engineStarted permanent-block bug (V2Bridge.EnsureEngineStarted) The flag was set BEFORE the existing-config guard, so a guarded early return left _engineStarted=true. A later call after the user confirmed replacement would hit if (_engineStarted) return; and never construct the engine — permanently locking the user out of local setup. Fix: move _engineStarted = true to AFTER all preflight guards pass. Also reset _engineStarted to false in the engineFactory catch path so transient construction failures are recoverable via Try-again. Mark the synthetic existing-config block as retryable so the user can confirm replace and retry without restarting onboarding. 2. Stale retry continuation race (V2Bridge.OnRetryRequested + EnsureEngineStarted) Added monotonic _engineGeneration counter. Bumped when retry resets engine state. Captured in the RunLocalOnlyAsync().ContinueWith(...) before the new run starts. Continuation no-ops if generation has been bumped — preventing an old run's final state from auto-advancing the V2 flow (LocalSetupProgress → GatewayWelcome) after the user clicked "Try again". 3. Try-again rendered for terminal/blocked failures (V2State + Bridge + Page) Added OnboardingV2State.LocalSetupCanRetry (default false). Bridge OnEngineStateChanged sets it true ONLY for FailedRetryable; terminal and blocked failures clear it. LocalSetupProgressPage.BuildErrorCard accepts a nullable Action? onTryAgain and omits the button when null; single-column grid layout when no retry button is shown. Bridge OnRetryRequested is gated on LocalSetupCanRetry as defense-in-depth so a stale UI event from before the page re-rendered cannot restart the engine on a terminal failure. 4. New source projects added to slnx Added OpenClawTray.OnboardingV2 (no platform mapping) and OpenClaw.SetupPreview (with x64/ARM64 mapping like Tray.WinUI, since it's WindowsAppSDKSelfContained=true and AnyCPU would need a RID). Now visible in VS/Rider solution view. Tests added (OpenClawTray.OnboardingV2.Tests): - LocalSetupCanRetry_DefaultsToFalse - LocalSetupCanRetry_SetTrue_FiresStateChanged - LocalSetupCanRetry_SetSameValue_DoesNotFireStateChanged Bridge-level integration tests for fixes openclaw#1 and openclaw#2 (the simulation Scott asked for) require constructing OnboardingV2Bridge with a mock engine factory — but the bridge depends on App.xaml.cs (gateway client reseeding), which can't be easily unit-tested without WinUI runtime. The fixes are covered with strong inline contracts/comments documenting the invariants. Validation (worktree, OPENCLAW_REPO_ROOT set): - ./build.ps1 ✅ - Shared.Tests — 1548 passed / 28 skipped / 0 failed ✅ - Tray.Tests — 1197 passed / 0 failed ✅ (was 1178; +19 from master merge) - OpenClawTray.OnboardingV2.Tests — 7 passed / 0 failed ✅ (was 4; +3 CanRetry) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@shanselman thanks for the careful re-review. Pushed 136e3e0 (on top of merging current master) which addresses all four items:
Tests added in Validation (worktree,
Lower-priority items not addressed (per your note they're not blocking): hard-coded |
Two bugs surfaced during a live V2 setup-flow test, plus four hardening items from the Hanselman dual-model review of the fix. Live-test bugs: 1. "Gateway wizard not available" on first-run V2 setup The V2 GatewayWelcome page embeds the legacy WizardPage which polls App.GatewayClient for 30s waiting for IsConnectedToGateway==true, then calls wizard.start. Live test: zero wizard.start in the log; wizard fell through to its "offline" branch. Root cause: After PairAsync flips EnableNodeMode mid-onboarding, App.GatewayClient briefly points at the engine's bootstrap-pairing lifecycle (which still reports IsConnectedToGateway==true at the instant ScheduleAdvanceAfterCompletion ran). The previous code skipped ReconnectAsync because of that. The bootstrap socket then died after Phase 14/15 cleanup, leaving the wizard with a zombie client. Fix: ALWAYS call ConnectionManager.ReconnectAsync after engine Complete (no skip). Wrapped in Task.Run so we await it without blocking the UI dispatcher. Re-seed legacy.GatewayClient from the post-reconnect App.GatewayClient via the dispatcher. Added pre/post diagnostic log lines so future regressions are obvious. 2. "Back from Advanced Connection page goes to Permissions" In OnboardingWindow.OnRouteChanged, the V2 bridge-back route map had _ => V2Route.Permissions as catch-all. When the user hit Back on legacy Connection, the legacy state fired RouteChanged with OnboardingRoute.SetupWarning — which fell through to the catch-all and forward-jumped the user to V2 Permissions instead of back to V2 Welcome. Fix: explicit SetupWarning => V2.Welcome, plus Chat => V2.AllSet (don't demote a completed user back to Welcome). Hanselman pass-3 hardening (HIGH consensus from both Opus + Codex): 3. Reconnect stampede / Retry race If the user clicks Try-again mid-reconnect, two ScheduleAdvance... continuations can interleave. The legacy.GatewayClient setter disposes the previous value, so a stale post-reseed could yank a client out from under the wizard. Fix: capture _engineGeneration at ScheduleAdvanceAfterCompletion start; bail in the continuation (and again inside the dispatcher enqueue) if generation no longer matches. Piggybacks on the existing _engineGeneration counter that OnRetryRequested bumps. 4. Post-dispose continuation safety The Task.Run continuation (and its dispatcher enqueue) could fire after the bridge is disposed, mutating a disposed OnboardingState and possibly double-disposing GatewayClient via the setter. Fix: check _disposed at three points — entry to the continuation, inside the dispatcher enqueue, AND in the existing 1-second advance-delay continuation. Belt-and-braces. Hanselman pass-3 LOW (cheap defensive fixes): 5. Drop the "immediate seed" of legacy.GatewayClient with the pre-reconnect (zombie) value. The whole point of the fix is to replace that client; seeding it just gave non-V2 consumers the broken client until ReconnectAsync completed. Now legacy.GatewayClient is set exactly once, by the post-reconnect dispatcher continuation. 6. Log a Warn when the bridge-back route catch-all hits an unmapped legacy route, so future enum additions surface in testing instead of silently landing the user on V2 Welcome. Hanselman pass-3 LOW (deliberately not addressed): - Opus flagged a theoretical concern that SetupWarning could fire on the FORWARD path to Connection (not just on Back), bouncing the user back to V2 Welcome. Codex's symmetric-defect check explicitly investigated this and concluded the legacy OnboardingApp only fires NotifyRouteChanged from GoNext/GoBack, never on initial mount. The live test of the Back-from-Connection fix worked correctly. If we ever change the legacy app to fire RouteChanged on mount, this becomes a real concern — addressing then. Validation (worktree, OPENCLAW_REPO_ROOT set): - ./build.ps1 — all 4 projects ✅ - Shared.Tests — 1548 passed / 28 skipped / 0 failed ✅ - Tray.Tests — 1197 passed / 0 failed ✅ - OpenClawTray.OnboardingV2.Tests — 7 passed / 0 failed ✅ Manual test (live): - Reset state + relaunch + V2 setup → completes through Welcome, LocalSetupProgress, GatewayWelcome (with wizard), Permissions, AllSet. - V2 Welcome → Advanced setup → Back on legacy Connection → returns to V2 Welcome (was previously jumping to V2 Permissions). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@shanselman quick re-review ping — head is now
Plus 3 new
Validation (worktree,
Live manual test: V2 setup completes → wizard mounts with provider/model picker → Permissions → AllSet. V2 Welcome → Advanced → Back returns to V2 Welcome correctly. Lower-priority items from your earlier review still open (per your note, not blocking):
Happy to do those in a follow-up if you'd like. Otherwise this should be ready for another look. |
Four items flagged by @shanselman after re-reviewing 488c94f. All four addressed in this commit: 1. _engineStarted permanent-block bug (V2Bridge.EnsureEngineStarted) The flag was set BEFORE the existing-config guard, so a guarded early return left _engineStarted=true. A later call after the user confirmed replacement would hit if (_engineStarted) return; and never construct the engine — permanently locking the user out of local setup. Fix: move _engineStarted = true to AFTER all preflight guards pass. Also reset _engineStarted to false in the engineFactory catch path so transient construction failures are recoverable via Try-again. Mark the synthetic existing-config block as retryable so the user can confirm replace and retry without restarting onboarding. 2. Stale retry continuation race (V2Bridge.OnRetryRequested + EnsureEngineStarted) Added monotonic _engineGeneration counter. Bumped when retry resets engine state. Captured in the RunLocalOnlyAsync().ContinueWith(...) before the new run starts. Continuation no-ops if generation has been bumped — preventing an old run's final state from auto-advancing the V2 flow (LocalSetupProgress → GatewayWelcome) after the user clicked "Try again". 3. Try-again rendered for terminal/blocked failures (V2State + Bridge + Page) Added OnboardingV2State.LocalSetupCanRetry (default false). Bridge OnEngineStateChanged sets it true ONLY for FailedRetryable; terminal and blocked failures clear it. LocalSetupProgressPage.BuildErrorCard accepts a nullable Action? onTryAgain and omits the button when null; single-column grid layout when no retry button is shown. Bridge OnRetryRequested is gated on LocalSetupCanRetry as defense-in-depth so a stale UI event from before the page re-rendered cannot restart the engine on a terminal failure. 4. New source projects added to slnx Added OpenClawTray.OnboardingV2 (no platform mapping) and OpenClaw.SetupPreview (with x64/ARM64 mapping like Tray.WinUI, since it's WindowsAppSDKSelfContained=true and AnyCPU would need a RID). Now visible in VS/Rider solution view. Tests added (OpenClawTray.OnboardingV2.Tests): - LocalSetupCanRetry_DefaultsToFalse - LocalSetupCanRetry_SetTrue_FiresStateChanged - LocalSetupCanRetry_SetSameValue_DoesNotFireStateChanged Bridge-level integration tests for fixes openclaw#1 and openclaw#2 (the simulation Scott asked for) require constructing OnboardingV2Bridge with a mock engine factory — but the bridge depends on App.xaml.cs (gateway client reseeding), which can't be easily unit-tested without WinUI runtime. The fixes are covered with strong inline contracts/comments documenting the invariants. Validation (worktree, OPENCLAW_REPO_ROOT set): - ./build.ps1 ✅ - Shared.Tests — 1548 passed / 28 skipped / 0 failed ✅ - Tray.Tests — 1197 passed / 0 failed ✅ (was 1178; +19 from master merge) - OpenClawTray.OnboardingV2.Tests — 7 passed / 0 failed ✅ (was 4; +3 CanRetry) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Superseded by #411, which has merged. |
V2 onboarding redesign
Replaces the legacy onboarding wizard with a redesigned 5-page V2 flow built from designer mocks (
tools/v2-design-refs/). V2 is mounted by default inOnboardingWindow;OPENCLAW_USE_V2_SETUP=0is a kill-switch fallback to legacy for one release cycle.Pages (in order): Welcome → LocalSetupProgress → GatewayWelcome → Permissions → AllSet
Architecture
src/OpenClawTray.OnboardingV2/— new class library: state, app shell, 5 page components, animations, theme palette, V2Strings, system theme detection. Builds againstOpenClawTray.FunctionalUI(the existing minimal-Reactor framework).src/OpenClaw.SetupPreview/— standalone WinUI 3 unpackaged exe for the inner-loop. Mounts the V2 tree against a fakeOnboardingV2State. Headless capture mode for visual diffs; F2 to cycle themes; fake stage-progression timer for designer review.src/OpenClaw.Tray.WinUI/Onboarding/V2/OnboardingV2Bridge.cs— translates real tray services toOnboardingV2State. All cross-thread mutations marshal throughDispatcherQueue.src/OpenClaw.Tray.WinUI/Onboarding/OnboardingWindow.cs— host: V2/legacy mount selection, Advanced→legacy round-trip with bridge-back, completion re-keyed for V2 AllSet.tools/v2_visual_diff.py— renders V2 pages headless via SetupPreview and produces side-by-side comparisons against designer PNGs (LLM eval rather than pixel diff per design-team request).tools/seed_v2_resw.py— idempotent script that adds every V2_* key to all 5 .resw locales.Service wiring (bridge)
LocalGatewaySetupEngine.StateChangedLocalSetupRows,LocalSetupErrorMessageLocalSetupProgressStageMap(no fork — V2 picks up every legacy bug-fix)PermissionChecker.CheckAllAsync+SubscribeToAccessChangesPermissions(PermissionRowSnapshotlist)Settings.GetEffectiveGatewayUrlGatewayUrlws://→http://flip for browser-launch linkSettings.AutoStart↔LaunchAtStartupLaunchAtStartupSettings.Save()Settings.EnableNodeModeNodeModeActiveOnboardingExistingConfigGuard.GetSummary()ExistingConfigsnapshotApp.ConnectionManager.ReconnectAsync()Easy-button path is logic-identical to v1
The
OnboardingV2Bridgerestores v1LocalSetupProgressPagebehaviour 1:1:replaceConfirmedflag forwarded from V2 state to engine factory and to legacyOnboardingStateConnectionManager.ReconnectAsync, seedslegacyState.GatewayClient)_advanceFiredForCompletioninstance guardengine_construct_failedsurfaced as user-facing errorlastRunningPhasederived fromstate.HistoryThe only logic changes are around Advanced setup and the new V2 flow shell.
Theme support
V2Theme.cscentralises every theme-aware brush (window/card backgrounds, text, accents, error/warning cards, transparent button overlays, step-dot inactive). Dark mirrors designer mocks; light patterns off WinUI 3 Fluent defaults; brand accents constant across themes per designer spec.V2SystemTheme.csresolves Windows app-mode color viaUISettings.GetColorValue(Foreground)(foreground white = dark mode, black = light mode).Application.Current.RequestedThemeis unreliable on unpackaged WinUI 3 apps.OnboardingV2State.ThemeMode(System/Light/Dark) drivesEffectiveTheme. Bridge resubscribes toUISettings.ColorValuesChangedso live system theme changes flip the UI.V2Theme. Permissions row icons sit inside a constant-colour dark badge so the designer's white-on-dark PNGs stay visible on light cards.Advanced setup → legacy Connection round-trip
Welcome's "Advanced setup" link raises
V2State.AdvancedSetupRequested→ host catches it via the bridge and re-mounts the sameFunctionalHostControlonto the legacyOnboardingAppatOnboardingRoute.Connection(no second window). LegacyOnboardingStateis constructed up-front so it's available for this swap.After legacy Connection completes (RouteChanged past Connection),
OnRouteChangedre-mounts V2 at the closest equivalent route. Wizard / Permissions / Ready all map to V2 Permissions — Advanced assumes the user is connecting to an already-configured gateway, so the legacy wizard step is intentionally skipped.A fresh
OnboardingV2Bridgeis created on bridge-back so the V2 tail (Permissions → AllSet) has live Finished / AutoStart-persist / Refresh / engine wiring.Cutover (
docs/ONBOARDING_V2.md)OnboardingWindow.TryCompleteOnboardingrecognises both legacyOnboardingRoute.ReadyandV2Route.AllSetas terminalsV2Strings.Resolver = LocalizationHelper.GetStringso V2 reads from the same.reswresources as legacytools/seed_v2_resw.py.LocalizationValidationTeststaught (key.StartsWith("V2_")) that V2 strings are intentionally English-only at first shipLocalSetupProgressStageMapreused (NOT forked) for stage→row mappingHanselman dual-model code review
Ran an adversarial Opus + Codex review against the branch. 9 findings:
Permissionsauto-property doesn'tNotifyChanged— V2 never re-rendered real permission stateRetryRequestedevent + bridge handler resets engine statePreviewWindowUISettings subscription leakNodeModeActiveone-way OR latchMapToV2Rows(V2Stage)icast assumes orderingStageOrderarray + length-mismatch guardPermissionsPage.AllGrantedstatic V2Strings initOnboardingCompletionPolicydoesn't gate V2 AllSetValidation
./build.ps1✅ all four projectsOpenClaw.Shared.Tests✅ 1548 passed / 28 skipped / 0 failedOpenClaw.Tray.Tests✅ 1164 passed / 0 failedManual test pass
Multiple full end-to-end runs from a clean slate (
./scripts/dev-reset-rebuild-launch.ps1 -SkipBuild -DontLaunch -WipeWslDistro):Out of scope (follow-up PRs)
OnboardingChatBootstrapper.BootstrapAsync) only runs in the WebView2 chat surface; the native FunctionalUI chat surface (default, added in08cab69) doesn't have an equivalent. My V2 path didn't touch any chat code.WizardPageis embedded inside V2GatewayWelcomevia a factory closure. The V2 chrome owns the title; the legacy wizard owns the body. A future PR should redesign the wizard step to share V2 card chrome.OnboardingApp+WelcomePage+SetupWarning+Wizard+LocalSetupProgress(legacy) +Permissions(legacy) +Ready+Chatare dead code in V2 mode but reachable via kill-switch + Advanced fallback. Deleting requires test refactoring; scope as its own PR.OPENCLAW_USE_V2_SETUP=0kill-switch after one release cycle on V2.Co-author
Co-authored with Copilot.