Skip to content

Upgrade Uno to 6.7.22 and fix left-nav selection after new session - #174

Merged
YoungSx merged 17 commits into
mainfrom
fix-nav-focus-issue-171
Sep 4, 2026
Merged

Upgrade Uno to 6.7.22 and fix left-nav selection after new session#174
YoungSx merged 17 commits into
mainfrom
fix-nav-focus-issue-171

Conversation

@YoungSx

@YoungSx YoungSx commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #171

Summary

Creating a session from the Start page left the left-nav selection visual on a different session in the same project. Two independent causes, one upstream and one ours.

Upstream — Uno.Sdk 6.6.29 → 6.7.22. 6.7 is the first stable line containing unoplatform/uno#21132, which re-syncs NavigationView with the latest WinUI sources and fixes the container-recycling selection defect. Verified by containment rather than by release notes: compare/release/stable/6.7...bdee6d9e reports ahead_by=0, while release/stable/6.6 is diverged. 6.8 is not available — NuGet only publishes 6.8.0-dev.* and global.json sets allowPrerelease: false.

Ours — the selection projection lied, and it mutated the tree under itself.

ApplySelectionProjection suppressed the SelectedItem notification whenever the newly projected item was reference-equal to the previously projected one. That guard is deliberate (republishing on every pass is what stranded the gray selection mask across rows) but it keyed on the wrong thing: SyncSessions reuses the row view models on purpose, so inserting a newly created session at the top of its project changes the menu source while leaving the projection reference-equal. Every container below the insert then renders a different row, the control is never told, and a OneWay binding gives no readback to notice. The contract the existing tests already describe is "stay quiet while the native menu source is stable" — so this tracks a structural revision of Items and of each project's Children and re-publishes once per revision. Pane toggles and no-op refreshes stay quiet exactly as before.

TryMaterializeSession also called RebuildTreeCore() synchronously from inside selection change notifications, mutating the bound collections under the handler that raised them — and RebuildTreeCore ends in NormalizeSelectionAfterRebuild, so the path re-entered itself. It now goes through the existing coalescing scheduler.

Changes

Commit What
build(uno) Uno.Sdk 6.7.22, Uno.WinUI.Lottie 6.7.103, docs; qualify the WebView2 control type
chore(styles) Re-anchor the NumberBox override copy to Uno 6.7.103
fix(nav) Structural-revision re-publish + coalesced session materialization

WebView2 needed qualifying because 6.7 pulls in the transitive WebView2Aot package, whose WebView2.dll declares a top-level WebView2 namespace. A namespace in the global namespace wins simple-name lookup over a using-imported type, so the bare name stopped resolving to the XAML control (CS0118).

The NumberBox override is a copy of the framework template kept for the still-open unoplatform/uno#24021, pinned to an upstream commit that a compliance test asserts. Rather than bump the string, the upstream template was diffed between the 6.6.166 commit and 6.7: exactly one change, PlaceholderTextContentPresenter gaining AutomationProperties.AccessibilityView="Raw". Adopted, then re-anchored.

Tested

  • Presentation.Core suite: 3293 passed / 0 failed
  • Nav selection class (incl. 2 new tests): 63/63 · Theme resource compliance: 26/26
  • net10.0-desktop Release: 0 warnings, 0 errors
  • No new warnings in any touched file
  • Reverse-verified: reverting the two implementation edits turns exactly the two new tests red (2 failed / 61 passed), so each test pins its own fix

New tests:

  • RebuildTree_RepublishesSelectedItemBinding_WhenNewerSessionIsInsertedAboveSelection — asserts the projected instance is unchanged yet a notification still fires, which is precisely what an instance-keyed guard misses
  • SessionSelectionWithoutNavRow_SchedulesRebuild_RatherThanMutatingTheBoundTreeInline — uses QueuedSynchronizationContext to assert the tree is untouched at selection time and only materializes after the queue drains

Not verified

  • net10.0-browserwasm was not built. It fails UNOWA0001 on this arm64 machine. Attributed, not assumed: WasmNativeWorkloadAvailable is false on both 6.6.29 and 6.7.22, and both bootstrap versions carry the diagnostic — so this predates the upgrade. Needs CI.
  • Windows MSIX and Skia GUI smoke were not run (MSIX is not possible on this host).
  • The AppWindow.Closing remark in MainPage.Shutdown.cs cites a decompile of 6.6.166. Only the symbol's presence was re-checked on 6.7.103, not each host's value; the comment now says so rather than silently re-pointing at an unverified version.

🤖 Generated with Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
salmonegg Ready Ready Preview Sep 4, 2026 2:20pm UTC

YoungSx added a commit that referenced this pull request Sep 4, 2026
Every gate workflow filtered pull_request to `branches: [develop]`, so a
PR opened against main ran no gates at all - the seven workflows only
fired on `push`, which is after the merge. A develop-targeted PR collects
14 checks; #174 against main collected two, both Vercel deploy previews.

That leaves main-targeted work unguarded exactly where it matters most:
Browser WASM Smoke Gates, Windows MSIX Package and Skia Desktop GUI Smoke
cannot run on an arm64 Linux dev box, so for those targets the pull
request gate is the only pre-merge signal that exists.

`push` already covered [main, develop] in all seven, so this only aligns
pull_request with it. No job, step, path filter or permission changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
YoungSx and others added 11 commits September 4, 2026 14:10
6.7 is the first stable line that contains unoplatform/uno#21132, which
re-syncs NavigationView with the latest WinUI sources and fixes the
container-recycling selection defect behind #171. 6.8 is not an option:
NuGet only publishes 6.8.0-dev.* and global.json sets
allowPrerelease: false.

Uno.WinUI.Lottie is the only Uno package pinned explicitly, so it moves
with the line (6.6.166 -> 6.7.103); the rest stay implicit Uno.Sdk
versions.

6.7 also pulls in the transitive WebView2Aot package, whose WebView2.dll
declares a top-level "WebView2" namespace. A namespace in the global
namespace wins simple-name lookup over a using-imported type, so the
bare name in XtermTerminalView stopped resolving to the XAML control
(CS0118); name the control type in full.

net10.0-desktop builds Release with 0 warnings and 0 errors and the
Presentation.Core suite is green. net10.0-browserwasm still fails
UNOWA0001 on this machine, unchanged from 6.6.29 -
WasmNativeWorkloadAvailable is false on both SDK versions - so the WASM
head needs CI to verify.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UnoNumberBoxThemeOverrides.xaml is a copy of the framework template kept
for unoplatform/uno#24021, which is still open, so the override stays.
Its provenance comment pins an upstream version and a compliance test
asserts that string, so the copy has to be re-checked against the new
line rather than have the number bumped.

Diffing the upstream template between the 6.6.166 commit and 6.7 shows a
single change: PlaceholderTextContentPresenter now leaves the
accessibility tree via AutomationProperties.AccessibilityView="Raw".
Adopt it, then re-anchor the comment and the assertion to 6.7.103.

The AppWindow.Closing remark in MainPage.Shutdown.cs cites a decompile of
6.6.166. Only the symbol's presence was re-checked on 6.7.103, not each
host's value, so record that limit instead of silently re-pointing the
citation at a version nobody verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Creating a session from the Start page left the pane's selection visual on
a different session in the same project (#171).

ApplySelectionProjection suppressed the SelectedItem notification whenever
the newly projected item was reference-equal to the previously projected
one. That guard is deliberate - republishing on every pass is what
stranded the gray selection mask across rows - but it keyed on the wrong
thing. SyncSessions reuses the row view models on purpose, so inserting a
newly created session at the top of its project changes the menu source
while leaving the projection reference-equal: every container below the
insert now renders a different row, the control is never told, and a
OneWay binding offers no readback to notice. The contract the existing
tests describe is "stay quiet while the native menu source is stable", so
track a structural revision of Items and of each project's Children and
re-publish once per revision. Pane toggles and no-op refreshes stay quiet
exactly as before.

TryMaterializeSession also called RebuildTreeCore() synchronously from
inside selection change notifications, mutating the bound collections
under the handler that raised them - and RebuildTreeCore ends in
NormalizeSelectionAfterRebuild, so the path re-entered itself. Route it
through the existing coalescing scheduler, which folds a request raised
mid-rebuild into the pending flag and re-schedules it as the running pass
unwinds. It still reports that this pass must not publish, because the
projection available before materialization resolves to a null selected
item and pushing that null costs NavigationView its IsChildSelected
ancestor visual.

Both changes are pinned by tests that were reverse-verified: reverting the
two implementation edits turns exactly these two tests red and nothing
else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
openApp threw a bare `page.waitForSelector: Timeout 180000ms exceeded` and
nothing else, so a boot regression was indistinguishable from a slow cold
start. Everything that explains it - a runtime abort, a framework asset
that 404s, a JS module that failed to load - had already gone by as console
output, and assertNoFatalConsoleMessages never ran because openApp threw
first. fatalConsolePattern also keeps only a known-fatal subset, so a new
failure shape is dropped by design.

Keep a bounded tail of every console and pageerror entry alongside the
fatal-filtered list, then attach page state plus both buffers to the error
openApp raises. The WASM head cannot be built on every contributor machine,
so the gate log has to be self-describing; "reproduce it locally" is not
available as a fallback.

Diagnostics only: the capture is additive and the new text is produced on
the throw path, so a passing smoke behaves exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first diagnostics pass proved the WASM app renders on Uno 6.7 - title,
body text and readyState were all correct with zero console or page errors -
and that not one element carried aria-label. Uno's WASM semantic DOM decides
per ARIA role whether a name lands on aria-label, on aria-labelledby, or as
text content, so an empty aria-label set says nothing about which selector
would match.

Report the attribute names the page actually carries plus a body outerHTML
sample, so the locator strategy can be fixed against observed DOM instead of
a guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uno 6.7's legacy WebAssembly DOM backend still renders the app - title, body
text and readyState were all correct with zero console or page errors - but no
element carries an accessible name any more. The instrumented smoke gate showed
the page's only identifying attributes are id, role and three data-* hooks, with
`xamltype`/`uno-*` classes on each div and not one aria-* name. We author
AutomationProperties.Name across 13 XAML files, so this is a screen-reader
regression for real users, not merely a broken test locator; adapting the
smoke selectors would have hidden it.

Every accessibility fix in the 6.7 line targets the Skia WASM semantic DOM
instead of the DOM backend (unoplatform/uno#23456, #23722, #24383), and Uno 7
removes the Android, iOS and WebAssembly DOM backends outright - "rendering is
Skia everywhere". So the browser head moves to the renderer desktop already
uses rather than staying on a backend upstream no longer maintains.

Verified locally: browserwasm now resolves UnoFeatures ";skiarenderer;webview;",
desktop is unchanged, and all three WasmShellNativeFileReference JS modules plus
WasmShellEnableIDBFS survive the switch. Rendering, input and the smoke gates
can only be confirmed in CI - the WASM head does not build on an arm64 dev box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ocating

With the browser head on Skia, aria-label came back but every [aria-label]
locator still waited out its full timeout. The diagnostics explained why:
bodyText was empty and the whole body was an "Enable accessibility" button
plus an "Application content" placeholder. Skia paints into a <canvas>, so the
accessibility tree is the only DOM Uno mirrors, and it builds that lazily -
until a screen reader is detected or that affordance is activated, no
AutomationProperties.Name reaches the DOM. Playwright is not a screen reader.

Activate it in openApp so all six WASM smokes get the tree. The affordance
renders empty despite role="button" and tabindex="0", so it has no hit box:
dispatch focus, click and Enter on the element rather than aiming a pointer at
it. A missing affordance is not an error, since the DOM-rendered heads never
had one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…one matched

Activating the accessibility tree got openApp past first paint, and the smoke
now fails one layer deeper: no visible navigation item for
`TitleBar.ToggleSidebar`. The candidate dump shows why. Every element reports
`automationId: ""` - Skia's semantic DOM publishes no data-automation-*
attribute - and the target is there under the accessible name "Toggle sidebar".
So aria-label now carries AutomationProperties.Name where one is authored and
falls back to the AutomationId otherwise, which is why id-only elements such as
StartView.Title still match while named ones do not.

The matcher already probed three data-automation spellings; add the plain id as
a last resort in case that is where the AutomationId now lives, and report both
the raw id and the accessible name in the candidate dump so a miss names which
identifier Skia published rather than only which one we asked for.

Diagnostic and additive: the extra probe cannot displace an existing match, and
41 automationIds call sites across eight smokes are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The candidate dumps showed every element reporting an empty automation id and
the DOM id being uno-semantics-<handle>, which led me toward reconstructing an
AutomationId-to-accessible-name map from XAML and resw. Uno's own reference makes
that unnecessary and says why the ids were missing:

  AutomationId | Web (WASM) | Skia / Native
    -> `xamlautomationid` attribute + `aria-label` on the HTML element
  "To avoid performance overhead, AutomationId only has an effect when the
   IsUiAutomationMappingEnabled MSBuild property is set to true"

So AutomationId was never being published - the mapping is opt-in - and the
attribute to read is `xamlautomationid`, which the matcher did not probe. It is
not a name-versus-id conflict and needs no mapping table: the 38 automationIds
call sites across eight smokes are correct as written.

Enable the mapping in the gate build rather than the project file, because the
documented reason it defaults off is runtime cost; shipped builds keep it off.
The clean step takes the same property so an incremental run cannot serve output
built without it. Probe `xamlautomationid` first and keep the data-automation-*
spellings for the older heads; drop the speculative plain-id probe.

Ref: Uno docs features/accessibility/automation-properties.md (release/stable/6.7).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The settings-navigation smoke was failing on the overflow menu, and the reason it
was fragile is the same reason it was hard to read: it described a click sequence
rather than a goal, and the parts it leaned on were implementation, not contract.

Four couplings removed:

- The overflow affordance was found by the Segoe Fluent private-use glyphs 
  and , then re-targeted through Uno's internal `.uno-button` class. A new
  icon or a renamed class broke the gate with no behaviour change. It is now found
  by accessible name, which is what Uno publishes and what a screen reader uses.
- Arrival was judged only by a multi-language prose alternation over body text.
  Such a pattern is satisfied by any one branch appearing anywhere on the page, so
  it cannot tell "arrived" from "that word is also here". The section must now
  report itself active - selected tab, heading, or aria-current - which is the
  state the navigation is supposed to produce. The caller's pattern is still
  honoured, but it is no longer the only evidence.
- navigateToSettingsSection resized the window to force the overflow route and
  restored it in a `finally`, while the smoke performed the same resize and
  overflow click again. Two viewport mutations made their order load-bearing.
  The caller now owns its viewport; the helper owns route selection.
- Locale is pinned to en-US when the context is created. Accessible names are
  localized by design, so an unpinned locale is what pushed every assertion into a
  language alternation in the first place.

Rect-based activation is kept deliberately: Uno's semantic nodes are a visually
hidden overlay beside the canvas, so Playwright's actionability checks refuse to
click them. That is a renderer property, not a shortcut.

navigateToSettingsSection keeps its signature - 24 call sites across six smokes
depend on it - so this changes how it identifies and verifies, not its contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The WASM smokes were coordinate-geometry scrums: rectangle centers, Tab
loops, Escape retries, hardcoded ComboBox indexes, six near-identical
full-DOM scanners, and a bottom-right-button heuristic. All of it probed
renderer internals that nothing promises.

Uno's WASM semantic DOM (enabled by IsUiAutomationMappingEnabled) is the
stable contract: nodes under #uno-semantics-root carry aria-label and
xamlautomationid, and Uno programs each node's click to its peer's
Invoke/Toggle/Selection/ExpandCollapse without hit testing - so program
matic element.click() is the documented activation route, needing no
scroll and no coordinate. Text boxes and sliders are real <input>s.

- ui-affordances.mjs: rewritten as thin node-side drivers over one
  in-page semantic runtime (window.__salmoneggSmoke.semantic, injected
  by browser-app.mjs). AutomationIds win over labels; failures dump the
  semantic DOM instead of guessed rectangles. Deleted
  findVisibleNavigationTargetPoint, the index maps, and the keyboard
  fallbacks.
- browser-app.mjs: inject the semantic runtime alongside the existing
  helper; findVisibleControl also honors xamlautomationid.
- settings-shell.mjs: rebuilt on the new primitives; overflow route
  tries semantic first, keeps the rect scanner only as a fallback for
  the template-generated overflow button whose name is not in our resw.
- gamepad smoke: Expander reveal prefers semantic toggle, keeps the
  real-mouse fallback (its unreliable-synthetic-click measurement
  predates the semantic contract); start-button click goes semantic.
- ChatInputArea: real AutomationIds for Send/Cancel, replacing the
  bottom-right button heuristic.

The named AutomationIds that the old heuristics guessed at are the same
ones screen readers announce - testing through them tests behaviour.

Co-Authored-By: Claude <noreply@anthropic.com>
YoungSx and others added 6 commits September 4, 2026 14:11
The section-active predicate waited for the navigation item to carry
aria-selected or aria-current. NavigationViewItem is rendered through
Uno's generic semantic path, which publishes neither attribute on Skia
WASM - so the wait could never succeed and burned its timeout before
every failure. Arrival was already asserted where it is observable:
the caller's bodyPattern pins strings that exist only on that section's
page.

Co-Authored-By: Claude <noreply@anthropic.com>
The smoke still queried [aria-label=<AutomationId>], the pre-6.7 DOM
shape. Under IsUiAutomationMappingEnabled the id lands on
xamlautomationid and the accessible name is the card title, so every
lookup returned null and the gate failed before reaching the tip-card
assertion. Locate the cards the same way every other smoke does, keep
the localized-title assertion as the accessible-name contract, and drop
the alpha/opacity checks that have no mirror in the semantic tree.

Co-Authored-By: Claude <noreply@anthropic.com>
These buttons carry no AutomationProperties.Name, so their aria-label only
coincidentally equals the automation id (the nameless fallback in the semantic
DOM). xamlautomationid is the id contract the semantic library itself matches
on, and it survives a Name being added later.

Co-Authored-By: Claude <noreply@anthropic.com>
The cards' AutomationId is x:Bind-fed, and Uno's BrowserWasm semantic mapping
bakes the automation id into the semantic element at node creation - before
bindings resolve - and never revises it, so the tree still carries the
button's x:Name fallback (HeroSuggestionButton) while the bound id never
appears. Static ids survive the bake; dynamic ones never do. Windows UIA
reads the bound id live, so the ViewModel ids remain valid for real
consumers - they are just unobservable through this tree. The behaviour a
screen reader consumer sees is a button announced by its localized title,
so match by name and pin the match to role=button.

Co-Authored-By: Claude <noreply@anthropic.com>
matchNode's label fallback accepts a textContent hit, and the hero card's
title TextBlock renders before its button's Name binding lands - so the
first poll settled on the title div. Add an optional role filter to
matchNode (no role passed: unchanged for every existing caller) and pin
the card waits and activation to role=button, letting the poll continue
until the announced button is actually there.

Co-Authored-By: Claude <noreply@anthropic.com>
…tic one

CI showed the hero card's command never fires through the semantic activate
path (element.click): the tip dialog never opens and the smoke times out on
its copy. That is the same BrowserWasm synthetic-click gap the expander
toggle and the gamepad refresh button already documented. Playwright's
trusted click on the same semantic node the screen reader exposes
([role=button][aria-label=...]) is the closest stand-in for the user's
gesture, and the old full-DOM version of this smoke was already passing
exactly that way.

Co-Authored-By: Claude <noreply@anthropic.com>
@YoungSx
YoungSx force-pushed the fix-nav-focus-issue-171 branch from 0c2febe to b7f5656 Compare September 4, 2026 14:11
@YoungSx
YoungSx merged commit 2cbd71d into main Sep 4, 2026
16 of 17 checks passed
@YoungSx
YoungSx deleted the fix-nav-focus-issue-171 branch September 4, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

从开始页新建对话后,左侧导航的焦点会落在同项目的其他会话上

1 participant